4 Commits

Author SHA1 Message Date
您的名字 514368bc07 feat: 升级到v3.1.0
- 实现定时按钮倒计时显示功能
- 适配pixel主题化图标展示
- 优化TimerDialog按钮宽度设计
2025-10-28 19:40:13 +08:00
您的名字 794e1a32fe feat: 优化播放进度条交互体验
- 修复拖拽时圆球消失问题
- 添加动态轨道高度变化效果(按住时4dp,松开时2dp)
- 优化圆球大小设置(固定14dp)
- 添加ProGuard规则保护DefaultTimeBar反射字段
- 改进触摸事件处理逻辑
- 增强拖拽体验的流畅性

修复内容:
- CustomSeekView: 重构触摸事件处理和动态高度调整
- 布局文件: 统一设置圆球大小为14dp
- ProGuard: 保护Media3 DefaultTimeBar字段不被混淆
2025-10-24 16:53:19 +08:00
您的名字 df0333d26e fix: 修复更新跳转链接,跳转到具体版本页面
- 修改Updater.java中的confirm方法
- 从硬编码的/releases/latest改为动态跳转到/releases/tag/v{version}
- 添加latestVersion字段存储检测到的最新版本号
- 确保点击更新后跳转到正确的版本页面
- 同时修复mobile和leanback版本
2025-10-24 14:42:58 +08:00
您的名字 0fd0e245d4 docs: 更新README.md到v3.0.9版本
- 更新版本号徽章到3.0.9
- 更新下载链接指向v3.0.9版本
- 添加v3.0.9版本历史记录
- 新增v3.0.9更新日志,包含新功能和UI优化
- 更新APK文件大小信息
2025-10-24 14:30:48 +08:00
79 changed files with 1232 additions and 119 deletions
+23 -4
View File
@@ -2,7 +2,7 @@
</h1> </h1>
<div align="center"> <div align="center">
![Version](https://img.shields.io/badge/version-3.0.8-blue.svg) ![Version](https://img.shields.io/badge/version-3.0.9-blue.svg)
![Android](https://img.shields.io/badge/platform-Android-green.svg) ![Android](https://img.shields.io/badge/platform-Android-green.svg)
![License](https://img.shields.io/badge/license-GPL--3.0-orange.svg) ![License](https://img.shields.io/badge/license-GPL--3.0-orange.svg)
![Build](https://img.shields.io/badge/build-passing-brightgreen.svg) ![Build](https://img.shields.io/badge/build-passing-brightgreen.svg)
@@ -36,14 +36,15 @@
## 📥 下载安装 ## 📥 下载安装
### 最新版本: v3.0.8 ### 最新版本: v3.0.9
| 平台 | ARM64-V8A | ARM V7A | | 平台 | ARM64-V8A | ARM V7A |
|------|-----------|---------| |------|-----------|---------|
| **📱 手机版** | [下载 (34MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.0.8/mobile-arm64_v8a-v3.0.8.apk) | [下载 (30MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.0.8/mobile-armeabi_v7a-v3.0.8.apk) | | **📱 手机版** | [下载 (35.8MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.0.9/mobile-arm64_v8a-v3.0.9.apk) | [下载 (31.6MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.0.9/mobile-armeabi_v7a-v3.0.9.apk) |
| **📺 TV版** | [下载 (34MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.0.8/leanback-arm64_v8a-v3.0.8.apk) | [下载 (30MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.0.8/leanback-armeabi_v7a-v3.0.8.apk) | | **📺 TV版** | [下载 (35.9MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.0.9/leanback-arm64_v8a-v3.0.9.apk) | [下载 (31.7MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.0.9/leanback-armeabi_v7a-v3.0.9.apk) |
### 📁 版本历史 ### 📁 版本历史
- **v3.0.9**: [查看v3.0.9版本](https://github.com/Tosencen/XMBOX-Release/tree/main/apk/release/v3.0.9) - 新增直播开关控制和UI交互优化
- **v3.0.8**: [查看v3.0.8版本](https://github.com/Tosencen/XMBOX-Release/tree/main/apk/release/v3.0.8) - UI交互体验全面优化 - **v3.0.8**: [查看v3.0.8版本](https://github.com/Tosencen/XMBOX-Release/tree/main/apk/release/v3.0.8) - UI交互体验全面优化
- **v3.0.7**: [查看v3.0.7版本](https://github.com/Tosencen/XMBOX-Release/tree/main/apk/release/v3.0.7) - 全面优化稳定性和用户体验 - **v3.0.7**: [查看v3.0.7版本](https://github.com/Tosencen/XMBOX-Release/tree/main/apk/release/v3.0.7) - 全面优化稳定性和用户体验
@@ -132,6 +133,24 @@ XMBOX/
## 📝 更新日志 ## 📝 更新日志
### v3.0.9 (2025-10-24)
#### ✨ 新功能
* **直播开关控制** - 新增直播tab显示/隐藏开关,用户可根据需要控制直播功能
* **实时倍速显示** - 播放控制对话框新增实时倍速数值显示,提升用户体验
* **源管理优化** - 优化源管理模块间距动态调整,界面更加协调
#### 🎨 UI优化
* **滑杆交互优化** - 滑杆圆球大小优化至20dp直径,提升操作体验
* **刻度显示改进** - 改进滑杆刻度显示,非激活轨道显示刻度,激活轨道保持干净
* **播放进度条增强** - 增强播放进度条动态大小调整功能,修复圆球跳回问题
* **直播开关逻辑** - 完善直播开关逻辑和UI交互,确保功能一致性
#### 🔧 技术改进
* **优化内存使用** - 进一步优化内存管理机制
* **提升播放稳定性** - 增强播放器稳定性
* **增强UI交互体验** - 改进用户界面交互响应
### v3.0.8 (2025-10-14) ### v3.0.8 (2025-10-14)
#### 🎨 UI交互体验全面优化 #### 🎨 UI交互体验全面优化
+3 -3
View File
@@ -27,8 +27,8 @@ android {
minSdk 24 minSdk 24
//noinspection ExpiredTargetSdkVersion //noinspection ExpiredTargetSdkVersion
targetSdk 28 targetSdk 28
versionCode 309 versionCode 310
versionName "3.0.9" versionName "3.1.0"
javaCompileOptions { javaCompileOptions {
annotationProcessorOptions { annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString(), "eventBusIndex": "com.fongmi.android.tv.event.EventIndex"] arguments = ["room.schemaLocation": "$projectDir/schemas".toString(), "eventBusIndex": "com.fongmi.android.tv.event.EventIndex"]
@@ -141,7 +141,7 @@ dependencies {
implementation 'com.github.jahirfiquitiva:TextDrawable:1.0.3' implementation 'com.github.jahirfiquitiva:TextDrawable:1.0.3'
implementation 'com.github.thegrizzlylabs:sardine-android:0.9' implementation 'com.github.thegrizzlylabs:sardine-android:0.9'
implementation 'com.github.teamnewpipe:NewPipeExtractor:v0.24.8' implementation 'com.github.teamnewpipe:NewPipeExtractor:v0.24.8'
implementation 'com.google.android.material:material:1.11.0' implementation 'com.google.android.material:material:1.12.0'
implementation 'com.google.zxing:core:3.5.3' implementation 'com.google.zxing:core:3.5.3'
implementation 'com.guolindev.permissionx:permissionx:1.7.1' implementation 'com.guolindev.permissionx:permissionx:1.7.1'
implementation 'com.hierynomus:smbj:0.13.0' implementation 'com.hierynomus:smbj:0.13.0'
+7
View File
@@ -92,5 +92,12 @@
-keep class com.sun.jna.** { *; } -keep class com.sun.jna.** { *; }
-keep class com.east.android.zlive.** { *; } -keep class com.east.android.zlive.** { *; }
# Media3 DefaultTimeBar - 保护反射访问的字段
-keep class androidx.media3.ui.DefaultTimeBar {
int barHeight;
int scrubberEnabledSize;
int scrubberDisabledSize;
}
# Zxing # Zxing
-keep class com.google.zxing.** { *; } -keep class com.google.zxing.** { *; }
@@ -32,6 +32,7 @@ public class Updater implements Download.Callback {
private AlertDialog dialog; private AlertDialog dialog;
private boolean dev; private boolean dev;
private boolean forceCheck; // 是否为手动检查 private boolean forceCheck; // 是否为手动检查
private String latestVersion; // 存储检测到的最新版本
private File getFile() { private File getFile() {
return Path.root("Download", "XMBOX-update.apk"); return Path.root("Download", "XMBOX-update.apk");
@@ -133,6 +134,7 @@ public class Updater implements Download.Callback {
String version = tagName.startsWith("v") ? tagName.substring(1) : tagName; String version = tagName.startsWith("v") ? tagName.substring(1) : tagName;
if (needUpdate(version)) { if (needUpdate(version)) {
this.latestVersion = version; // 保存最新版本号
App.post(() -> show(activity, version, body)); App.post(() -> show(activity, version, body));
} else { } else {
if (forceCheck) { if (forceCheck) {
@@ -205,9 +207,9 @@ public class Updater implements Download.Callback {
} }
private void confirm(View view) { private void confirm(View view) {
// 跳转到GitHub Releases页面而不是直接下载 // 跳转到具体版本的GitHub Releases页面
try { try {
String url = "https://github.com/Tosencen/XMBOX/releases/tag/v3.0.8"; String url = "https://github.com/Tosencen/XMBOX/releases/tag/v" + latestVersion;
Logger.d("Updater: Attempting to open URL: " + url); Logger.d("Updater: Attempting to open URL: " + url);
Intent intent = new Intent(Intent.ACTION_VIEW); Intent intent = new Intent(Intent.ACTION_VIEW);
@@ -14,11 +14,9 @@
android:valueFrom="1" android:valueFrom="1"
android:valueTo="10" android:valueTo="10"
app:thumbColor="@color/primary" app:thumbColor="@color/primary"
app:thumbRadius="10dp" app:thumbRadius="9dp"
app:tickVisible="true" app:tickVisible="false"
app:tickColor="@color/black_50"
app:trackColorActive="@color/primary" app:trackColorActive="@color/primary"
app:trackColorInactive="@color/white_20" app:trackColorInactive="@color/white_30" />
app:trackHeight="4dp" />
</FrameLayout> </FrameLayout>
@@ -29,8 +29,8 @@
android:nextFocusUp="@id/next" android:nextFocusUp="@id/next"
android:nextFocusDown="@id/timeBar" android:nextFocusDown="@id/timeBar"
app:bar_height="2dp" app:bar_height="2dp"
app:scrubber_enabled_size="12dp" app:scrubber_enabled_size="14dp"
app:scrubber_disabled_size="12dp" app:scrubber_disabled_size="14dp"
app:played_color="#FFEB3B" app:played_color="#FFEB3B"
app:scrubber_color="#FFEB3B" app:scrubber_color="#FFEB3B"
app:buffered_color="#80FFEB3B" app:buffered_color="#80FFEB3B"
Binary file not shown.
@@ -0,0 +1,270 @@
package com.fongmi.android.tv.api;
import java.util.Arrays;
import java.util.List;
/**
* 广告拦截器 - 内置常用广告域名库
*/
public class AdBlocker {
/**
* 赌博类广告域名(澳门新葡京等)
*/
private static final List<String> GAMBLING_ADS = Arrays.asList(
// 澳门博彩广告
".*\\..*葡京.*",
".*\\..*皇冠.*",
".*\\..*金沙.*",
".*\\..*威尼斯人.*",
".*\\..*永利.*",
".*aomen.*",
".*macau.*casino.*",
".*xpj.*\\..*",
".*xinpujing.*",
".*amdc.*\\.com",
".*\\.amdc\\.alipay\\.com",
// 常见博彩推广域名
".*\\.bz.*bet.*",
".*\\.casino.*",
".*\\.poker.*",
".*\\.betting.*",
".*\\.gamble.*",
".*wnsr.*\\..*",
".*js[0-9]+\\..*",
".*vn[0-9]+\\..*",
".*ag[0-9]+\\..*",
// 具体的博彩广告域名
"wan.51img1.com",
"iqiyi.hbuioo.com",
"vip.ffzyad.com",
"https.wshdsm.com",
"v.%E7%88%B1%E4%B8%8A%E5%A5%B9%E5%BD%B1%E9%99%A2.com"
);
/**
* 通用广告联盟域名
*/
private static final List<String> GENERAL_ADS = Arrays.asList(
// Google广告
"googleads.g.doubleclick.net",
"adservice.google.com",
"pagead2.googlesyndication.com",
"www.googletagmanager.com",
"static.doubleclick.net",
".*\\.doubleclick\\.net",
".*\\.googlesyndication\\.com",
// 百度广告
"cpro.baidu.com",
"pos.baidu.com",
"cbjs.baidu.com",
"hm.baidu.com",
".*\\.union\\.baidu\\.com",
// 淘宝/阿里广告
"mclick.simba.taobao.com",
"simba.m.taobao.com",
".*\\.tanx\\.com",
".*\\.mmstat\\.com",
".*\\.atm\\.youku\\.com",
// 腾讯广告
"mi.gdt.qq.com",
"adsmind.gdtimg.com",
".*\\.l\\.qq\\.com",
"pgdt.gtimg.cn",
// 其他主流广告联盟
"union.meituan.com",
"analytics.163.com",
"g.163.com",
"analytics.126.net",
".*\\.irs01\\.com",
".*\\.irs01\\.net"
);
/**
* 视频平台广告域名
*/
private static final List<String> VIDEO_ADS = Arrays.asList(
// 优酷广告
"atm.youku.com",
"stat.youku.com",
"ad.api.3g.youku.com",
"pl.youku.com",
"lstat.youku.com",
".*\\.atm\\.youku\\.com",
// 爱奇艺广告
"cupid.iqiyi.com",
"data.video.iqiyi.com",
"msg.71.am",
".*\\.cupid\\.iqiyi\\.com",
".*\\.data\\.video\\.iqiyi\\.com",
// 腾讯视频广告
"btrace.video.qq.com",
"mtrace.video.qq.com",
"vv.video.qq.com",
"ad.video.qq.com",
// 芒果TV广告
"da.mgtv.com",
"ad.hunantv.com",
"v2.hunantv.com",
// 其他视频平台
"ark.letv.com",
"stat.letv.com",
".*\\.beacon\\.qq\\.com"
);
/**
* 弹窗广告域名
*/
private static final List<String> POPUP_ADS = Arrays.asList(
// 常见弹窗广告
"mimg.0c1q0l.cn",
"www.92424.cn",
"k.jinxiuzhilv.com",
"cdn.bootcss.com",
"ppl.xunzhuo.com",
"xc.hubeijieshikj.cn",
"ssl.kdd.cc",
"push.zhanzhang.baidu.com",
"cpc.cmbchina.com",
"adshow.58.com",
// 移动端弹窗
"afp.csbew.com",
"aoodoo.feng.com",
"*.popin.cc",
"*.supersonicads.com"
);
/**
* 恶意网站和钓鱼网站
*/
private static final List<String> MALICIOUS_ADS = Arrays.asList(
".*\\.17un\\.com",
".*\\.baidustatic\\.com",
".*\\.cnzz\\.com",
".*\\.duomeng\\.cn",
".*\\.shuzilm\\.cn",
".*\\.haoyuemh\\.com",
".*\\.571xz\\.com",
".*\\.madthumbs\\.com"
);
/**
* 跟踪统计域名
*/
private static final List<String> TRACKING_ADS = Arrays.asList(
// 统计跟踪
"hm.baidu.com",
"tongji.baidu.com",
"s95.cnzz.com",
"cnzz.com",
".*\\.umeng\\.com",
".*\\.umtrack\\.com",
// Google Analytics
"www.google-analytics.com",
"ssl.google-analytics.com",
".*\\.googletagmanager\\.com"
);
/**
* 获取所有内置广告域名
* @return 完整的广告域名列表
*/
public static List<String> getAllAdHosts() {
return Arrays.asList(
// 合并所有列表
String.join(",", GAMBLING_ADS),
String.join(",", GENERAL_ADS),
String.join(",", VIDEO_ADS),
String.join(",", POPUP_ADS),
String.join(",", MALICIOUS_ADS),
String.join(",", TRACKING_ADS)
);
}
/**
* 获取赌博类广告域名(澳门新葡京等)
*/
public static List<String> getGamblingAdHosts() {
return GAMBLING_ADS;
}
/**
* 获取通用广告联盟域名
*/
public static List<String> getGeneralAdHosts() {
return GENERAL_ADS;
}
/**
* 获取视频平台广告域名
*/
public static List<String> getVideoAdHosts() {
return VIDEO_ADS;
}
/**
* 获取弹窗广告域名
*/
public static List<String> getPopupAdHosts() {
return POPUP_ADS;
}
/**
* 获取恶意网站域名
*/
public static List<String> getMaliciousAdHosts() {
return MALICIOUS_ADS;
}
/**
* 获取跟踪统计域名
*/
public static List<String> getTrackingAdHosts() {
return TRACKING_ADS;
}
/**
* 检查是否应该拦截该域名
* @param host 要检查的域名
* @return true=应该拦截, false=不拦截
*/
public static boolean shouldBlock(String host) {
if (host == null || host.isEmpty()) return false;
// 检查所有分类
return checkInList(host, GAMBLING_ADS) ||
checkInList(host, GENERAL_ADS) ||
checkInList(host, VIDEO_ADS) ||
checkInList(host, POPUP_ADS) ||
checkInList(host, MALICIOUS_ADS) ||
checkInList(host, TRACKING_ADS);
}
/**
* 检查域名是否在列表中(支持正则)
*/
private static boolean checkInList(String host, List<String> list) {
for (String pattern : list) {
if (host.matches(pattern.replace("*", ".*"))) {
return true;
}
if (host.contains(pattern.replace(".*", ""))) {
return true;
}
}
return false;
}
}
@@ -78,32 +78,41 @@ public class VodConfig {
} }
public static void load(Config config, Callback callback) { public static void load(Config config, Callback callback) {
android.util.Log.d("VodConfig", "load called with config: " + (config != null ? config.toString() : "null"));
// 参数检查 // 参数检查
if (config == null || callback == null) { if (config == null || callback == null) {
android.util.Log.e("VodConfig", "Invalid parameters: config=" + config + ", callback=" + callback);
if (callback != null) { if (callback != null) {
App.post(() -> callback.error("配置参数无效")); App.post(() -> callback.error("配置参数无效"));
} }
return; return;
} }
android.util.Log.d("VodConfig", "Parameters valid, proceeding with load");
// 添加加载状态检查,防止并发加载 // 添加加载状态检查,防止并发加载
VodConfig instance = get(); VodConfig instance = get();
synchronized (instance) { synchronized (instance) {
if (instance.isLoading) { if (instance.isLoading) {
android.util.Log.d("VodConfig", "Already loading, cancelling previous load");
// 如果正在加载,取消之前的加载 // 如果正在加载,取消之前的加载
try { try {
OkHttp.cancel("vod"); OkHttp.cancel("vod");
} catch (Exception e) { } catch (Exception e) {
android.util.Log.e("VodConfig", "Error cancelling previous load", e);
e.printStackTrace(); e.printStackTrace();
} }
} }
instance.isLoading = true; instance.isLoading = true;
} }
android.util.Log.d("VodConfig", "Calling instance.clear().config(config).load(callback)");
try { try {
instance.clear().config(config).load(callback); instance.clear().config(config).load(callback);
} catch (Exception e) { } catch (Exception e) {
instance.isLoading = false; instance.isLoading = false;
android.util.Log.e("VodConfig", "Exception during load", e);
e.printStackTrace(); e.printStackTrace();
App.post(() -> callback.error("配置加载失败: " + e.getMessage())); App.post(() -> callback.error("配置加载失败: " + e.getMessage()));
} }
@@ -224,14 +233,26 @@ public class VodConfig {
return; return;
} }
String spider = Json.safeString(object, "spider"); String spider = Json.safeString(object, "spider");
BaseLoader.get().parseJar(spider, true); try {
BaseLoader.get().parseJar(spider, true);
} catch (Throwable e) {
android.util.Log.e("VodConfig", "Failed to parse spider jar: " + spider, e);
e.printStackTrace();
}
for (JsonElement element : Json.safeListElement(object, "sites")) { for (JsonElement element : Json.safeListElement(object, "sites")) {
Site site = Site.objectFrom(element); try {
if (sites.contains(site)) continue; Site site = Site.objectFrom(element);
site.setApi(UrlUtil.convert(site.getApi())); if (sites.contains(site)) continue;
site.setExt(UrlUtil.convert(site.getExt())); site.setApi(UrlUtil.convert(site.getApi()));
site.setJar(parseJar(site, spider)); site.setExt(UrlUtil.convert(site.getExt()));
sites.add(site.trans().sync()); site.setJar(parseJar(site, spider));
sites.add(site.trans().sync());
} catch (Throwable e) {
android.util.Log.e("VodConfig", "Failed to add site: " + element, e);
e.printStackTrace();
// 继续处理下一个站点
}
} }
for (Site site : sites) { for (Site site : sites) {
if (site.getKey().equals(config.getHome())) { if (site.getKey().equals(config.getHome())) {
@@ -46,10 +46,15 @@ public class JarLoader {
} }
private void load(String key, File file) { private void load(String key, File file) {
if (!file.setReadOnly()) return; try {
loaders.put(key, dex(file)); if (!file.setReadOnly()) return;
invokeInit(key); loaders.put(key, dex(file));
putProxy(key); invokeInit(key);
putProxy(key);
} catch (Throwable e) {
android.util.Log.e("JarLoader", "Failed to load jar for key: " + key, e);
e.printStackTrace();
}
} }
private DexClassLoader dex(File file) { private DexClassLoader dex(File file) {
@@ -85,19 +90,24 @@ public class JarLoader {
} }
public synchronized void parseJar(String key, String jar) { public synchronized void parseJar(String key, String jar) {
if (loaders.containsKey(key)) return; try {
String[] texts = jar.split(";md5;"); if (loaders.containsKey(key)) return;
String md5 = texts.length > 1 ? texts[1].trim() : ""; String[] texts = jar.split(";md5;");
if (md5.startsWith("http")) md5 = OkHttp.string(md5).trim(); String md5 = texts.length > 1 ? texts[1].trim() : "";
jar = texts[0]; if (md5.startsWith("http")) md5 = OkHttp.string(md5).trim();
if (!md5.isEmpty() && Util.equals(jar, md5)) { jar = texts[0];
load(key, Path.jar(jar)); if (!md5.isEmpty() && Util.equals(jar, md5)) {
} else if (jar.startsWith("http")) { load(key, Path.jar(jar));
load(key, download(jar)); } else if (jar.startsWith("http")) {
} else if (jar.startsWith("file")) { load(key, download(jar));
load(key, Path.local(jar)); } else if (jar.startsWith("file")) {
} else if (jar.startsWith("assets")) { load(key, Path.local(jar));
parseJar(key, UrlUtil.convert(jar)); } else if (jar.startsWith("assets")) {
parseJar(key, UrlUtil.convert(jar));
}
} catch (Throwable e) {
android.util.Log.e("JarLoader", "Failed to parse jar for key: " + key + ", jar: " + jar, e);
e.printStackTrace();
} }
} }
@@ -59,22 +59,22 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
timeBar.addListener(this); timeBar.addListener(this);
refresh = this::refresh; refresh = this::refresh;
// 设置触摸事件监听器,实现动态尺寸调整 // 添加触摸事件处理,实现按住时圆球变大的效果
timeBar.setOnTouchListener((v, event) -> { timeBar.setOnTouchListener((v, event) -> {
switch (event.getAction()) { switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_DOWN:
if (!isPressed) { if (!isPressed) {
isPressed = true; isPressed = true;
// 按时:滑杆4dp,圆球16dp // 按时:轨道变高到4dp
setTimeBarSize(4, 16); setTimeBarHeight(4);
} }
break; break;
case MotionEvent.ACTION_UP: case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_CANCEL:
if (isPressed) { if (isPressed) {
isPressed = false; isPressed = false;
// 松开时:滑杆2dp,圆球12dp // 松开时:轨道恢复到2dp
setTimeBarSize(2, 12); setTimeBarHeight(2);
} }
break; break;
} }
@@ -95,54 +95,32 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
} }
/** /**
* 动态设置进度条高度和拖拽手柄大小 * 动态调整进度条高度
* @param barHeightDp 滑杆高度dp * @param barHeightDp 轨道高度(dp
* @param scrubberSizeDp 拖拽手柄大小(dp
*/ */
private void setTimeBarSize(int barHeightDp, int scrubberSizeDp) { private void setTimeBarHeight(int barHeightDp) {
// 设置滑杆高度
int barHeightPx = (int) TypedValue.applyDimension( int barHeightPx = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, TypedValue.COMPLEX_UNIT_DIP,
barHeightDp, barHeightDp,
getContext().getResources().getDisplayMetrics() getContext().getResources().getDisplayMetrics()
); );
// 设置拖拽手柄大小 // 尝试通过反射设置DefaultTimeBar的内部barHeight字段
int scrubberSizePx = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
scrubberSizeDp,
getContext().getResources().getDisplayMetrics()
);
// 通过反射设置DefaultTimeBar的内部属性
try { try {
// 设置滑杆高度
java.lang.reflect.Field barHeightField = timeBar.getClass().getDeclaredField("barHeight"); java.lang.reflect.Field barHeightField = timeBar.getClass().getDeclaredField("barHeight");
barHeightField.setAccessible(true); barHeightField.setAccessible(true);
barHeightField.setInt(timeBar, barHeightPx); barHeightField.setInt(timeBar, barHeightPx);
// 设置拖拽手柄大小 - 尝试多个可能的字段名 // 强制刷新
String[] scrubberFields = {"scrubberSize", "scrubberEnabledSize", "scrubberDisabledSize"};
for (String fieldName : scrubberFields) {
try {
java.lang.reflect.Field scrubberField = timeBar.getClass().getDeclaredField(fieldName);
scrubberField.setAccessible(true);
scrubberField.setInt(timeBar, scrubberSizePx);
break; // 成功设置后退出循环
} catch (NoSuchFieldException e) {
// 继续尝试下一个字段名
}
}
// 刷新视图
timeBar.requestLayout();
timeBar.invalidate(); timeBar.invalidate();
} catch (Exception e) {
// 如果反射失败,使用备用方案
e.printStackTrace();
// 备用方案:重新设置布局参数
timeBar.getLayoutParams().height = barHeightPx;
timeBar.requestLayout(); timeBar.requestLayout();
} catch (Exception e) {
// 如果反射失败,尝试调整布局参数
android.util.Log.w("CustomSeekView", "Failed to set bar height via reflection: " + e.getMessage());
if (timeBar.getLayoutParams() != null) {
timeBar.getLayoutParams().height = barHeightPx;
timeBar.requestLayout();
}
} }
} }
@@ -224,7 +202,7 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
positionView.setText(player.stringToTime(actualPosition)); positionView.setText(player.stringToTime(actualPosition));
} }
} }
}, 50); // 延迟50ms刷新 }, 100); // 增加延迟时间,确保拖拽状态完全结束
} }
@Override @Override
@@ -247,6 +225,7 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
@Override @Override
public void onScrubStop(@NonNull TimeBar timeBar, long position, boolean canceled) { public void onScrubStop(@NonNull TimeBar timeBar, long position, boolean canceled) {
scrubbing = false; scrubbing = false;
if (!canceled) { if (!canceled) {
// 立即设置进度条位置到目标位置,避免圆球跳回原始位置 // 立即设置进度条位置到目标位置,避免圆球跳回原始位置
timeBar.setPosition(position); timeBar.setPosition(position);
@@ -259,5 +238,7 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
player.play(); player.play();
} }
} }
// 不干预DefaultTimeBar的圆球绘制,让它自己处理
} }
} }
@@ -19,6 +19,7 @@ import androidx.annotation.NonNull;
import com.fongmi.android.tv.App; import com.fongmi.android.tv.App;
import com.fongmi.android.tv.Constant; import com.fongmi.android.tv.Constant;
import com.fongmi.android.tv.Setting; import com.fongmi.android.tv.Setting;
import com.fongmi.android.tv.api.AdBlocker;
import com.fongmi.android.tv.api.config.LiveConfig; import com.fongmi.android.tv.api.config.LiveConfig;
import com.fongmi.android.tv.api.config.VodConfig; import com.fongmi.android.tv.api.config.VodConfig;
import com.fongmi.android.tv.impl.ParseCallback; import com.fongmi.android.tv.impl.ParseCallback;
@@ -177,8 +178,13 @@ public class CustomWebView extends WebView implements DialogInterface.OnDismissL
} }
private boolean isAd(String host) { private boolean isAd(String host) {
// 1. 首先检查内置广告域名库(包含常见的澳门新葡京等赌博广告)
if (AdBlocker.shouldBlock(host)) return true;
// 2. 然后检查用户自定义的广告域名
for (String ad : VodConfig.get().getAds()) if (Util.containOrMatch(host, ad)) return true; for (String ad : VodConfig.get().getAds()) if (Util.containOrMatch(host, ad)) return true;
for (String ad : LiveConfig.get().getAds()) if (Util.containOrMatch(host, ad)) return true; for (String ad : LiveConfig.get().getAds()) if (Util.containOrMatch(host, ad)) return true;
return false; return false;
} }
+1 -1
View File
@@ -7,7 +7,7 @@
android:topLeftRadius="8dp" android:topLeftRadius="8dp"
android:topRightRadius="8dp" /> android:topRightRadius="8dp" />
<solid android:color="#B32196F3" /> <solid android:color="#B3000000" />
<padding <padding
android:bottom="4dp" android:bottom="4dp"
@@ -1,9 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white" /> <!-- 使用纯色背景,自动适配深浅色模式 -->
<background android:drawable="@color/launcher_background" />
<!-- 前景图标:使用 inset 缩小显示(因为图标铺满了画布)-->
<foreground> <foreground>
<inset <inset
android:drawable="@mipmap/ic_launcher_foreground" android:drawable="@mipmap/ic_launcher_foreground"
android:inset="20%" /> android:inset="20%" />
</foreground> </foreground>
<!-- 主题图标:也需要使用 inset 保持大小一致 -->
<monochrome>
<inset
android:drawable="@mipmap/ic_launcher_foreground"
android:inset="20%" />
</monochrome>
</adaptive-icon> </adaptive-icon>
@@ -1,9 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white" /> <!-- 使用纯色背景,自动适配深浅色模式 -->
<background android:drawable="@color/launcher_background" />
<!-- 前景图标:使用 inset 缩小显示(因为图标铺满了画布)-->
<foreground> <foreground>
<inset <inset
android:drawable="@mipmap/ic_launcher_foreground" android:drawable="@mipmap/ic_launcher_foreground"
android:inset="20%" /> android:inset="20%" />
</foreground> </foreground>
<!-- 主题图标:也需要使用 inset 保持大小一致 -->
<monochrome>
<inset
android:drawable="@mipmap/ic_launcher_foreground"
android:inset="20%" />
</monochrome>
</adaptive-icon> </adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

+7
View File
@@ -0,0 +1,7 @@
<resources>
<!-- Launcher icon background (Dark mode) -->
<color name="launcher_background">#222222</color>
</resources>
+1 -1
View File
@@ -91,7 +91,7 @@
<string name="setting_app">应用设置</string> <string name="setting_app">应用设置</string>
<string name="setting_network">网络设置</string> <string name="setting_network">网络设置</string>
<string name="setting_data">数据管理</string> <string name="setting_data">数据管理</string>
<string name="app_version">v3.0.3</string> <string name="app_version">v3.0.9</string>
<string name="about_github">在GitHub上查看</string> <string name="about_github">在GitHub上查看</string>
<!-- Backup & Restore --> <!-- Backup & Restore -->
+1 -1
View File
@@ -89,7 +89,7 @@
<string name="setting_app">應用設置</string> <string name="setting_app">應用設置</string>
<string name="setting_network">網絡設置</string> <string name="setting_network">網絡設置</string>
<string name="setting_data">數據管理</string> <string name="setting_data">數據管理</string>
<string name="app_version">v3.0.3</string> <string name="app_version">v3.0.9</string>
<string name="about_github">在GitHub上查看</string> <string name="about_github">在GitHub上查看</string>
<!-- Backup & Restore --> <!-- Backup & Restore -->
+3
View File
@@ -28,4 +28,7 @@
<color name="text_toast">#FFEB3B</color> <color name="text_toast">#FFEB3B</color>
<color name="yellow_500">#FFEB3B</color> <color name="yellow_500">#FFEB3B</color>
<!-- Launcher icon background (Light mode) -->
<color name="launcher_background">#FFFFFF</color>
</resources> </resources>
+4 -4
View File
@@ -92,7 +92,7 @@
<string name="setting_choose">Choose</string> <string name="setting_choose">Choose</string>
<string name="setting_off">Off</string> <string name="setting_off">Off</string>
<string name="setting_on">On</string> <string name="setting_on">On</string>
<string name="app_version">v3.0.6</string> <string name="app_version">v3.0.9</string>
<string name="about_github">View on GitHub</string> <string name="about_github">View on GitHub</string>
<!-- Backup & Restore --> <!-- Backup & Restore -->
@@ -152,7 +152,7 @@
<string name="error_device_limit">Device authorization limit reached</string> <string name="error_device_limit">Device authorization limit reached</string>
<string name="error_live_empty">This subscription has no live content</string> <string name="error_live_empty">This subscription has no live content</string>
<string name="error_no_live">Current source has no live content</string> <string name="error_no_live">Current source has no live content</string>
<string name="error_empty">这里撒子内容都没得</string> <string name="error_empty">空谷待音</string>
<string name="error_keep_empty">老表~没得收藏哈</string> <string name="error_keep_empty">老表~没得收藏哈</string>
<string name="error_search_empty">搜索无结果,换个关键词试试</string> <string name="error_search_empty">搜索无结果,换个关键词试试</string>
<string name="error_detail">No play data</string> <string name="error_detail">No play data</string>
@@ -238,8 +238,8 @@
<string name="target_size">Target size</string> <string name="target_size">Target size</string>
<string name="scan_result">Scan result</string> <string name="scan_result">Scan result</string>
<string name="source_hint">No video sources added yet\nClick the button below to add</string> <string name="source_hint">空谷无音,待君添源</string>
<string name="add_source">Add Source</string> <string name="add_source">添加源</string>
<!-- 隐私协议相关 --> <!-- 隐私协议相关 -->
<string name="privacy_agreement_title">XMBOX软件许可协议</string> <string name="privacy_agreement_title">XMBOX软件许可协议</string>
@@ -32,6 +32,7 @@ public class Updater implements Download.Callback {
private AlertDialog dialog; private AlertDialog dialog;
private boolean dev; private boolean dev;
private boolean forceCheck; // 是否为手动检查 private boolean forceCheck; // 是否为手动检查
private String latestVersion; // 存储检测到的最新版本
private File getFile() { private File getFile() {
return Path.root("Download", "XMBOX-update.apk"); return Path.root("Download", "XMBOX-update.apk");
@@ -148,6 +149,7 @@ public class Updater implements Download.Callback {
// 比较版本号 // 比较版本号
if (needUpdate(version)) { if (needUpdate(version)) {
Logger.d("Updater: Update needed, showing dialog"); Logger.d("Updater: Update needed, showing dialog");
this.latestVersion = version; // 保存最新版本号
App.post(() -> show(activity, version, body)); App.post(() -> show(activity, version, body));
} else { } else {
Logger.d("Updater: No update needed"); Logger.d("Updater: No update needed");
@@ -211,9 +213,9 @@ public class Updater implements Download.Callback {
} }
private void confirm(View view) { private void confirm(View view) {
// 跳转到GitHub Releases页面而不是直接下载 // 跳转到具体版本的GitHub Releases页面
try { try {
String url = "https://github.com/Tosencen/XMBOX/releases/tag/v3.0.8"; String url = "https://github.com/Tosencen/XMBOX/releases/tag/v" + latestVersion;
Logger.d("Updater: Attempting to open URL: " + url); Logger.d("Updater: Attempting to open URL: " + url);
Intent intent = new Intent(Intent.ACTION_VIEW); Intent intent = new Intent(Intent.ACTION_VIEW);
@@ -1,8 +1,10 @@
package com.fongmi.android.tv.ui.activity; package com.fongmi.android.tv.ui.activity;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
@@ -98,6 +100,8 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
private String tag; private String tag;
private int count; private int count;
private PiP mPiP; private PiP mPiP;
private BroadcastReceiver mScreenReceiver;
private boolean mPausedByScreen = false;
public static void start(Context context) { public static void start(Context context) {
if (!LiveConfig.isEmpty()) context.startActivity(new Intent(context, LiveActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).putExtra("empty", false)); if (!LiveConfig.isEmpty()) context.startActivity(new Intent(context, LiveActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).putExtra("empty", false));
@@ -148,6 +152,7 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
mR2 = this::setTraffic; mR2 = this::setTraffic;
mR3 = this::hideInfo; mR3 = this::hideInfo;
mPiP = new PiP(); mPiP = new PiP();
initScreenReceiver();
Server.get().start(); Server.get().start();
setRecyclerView(); setRecyclerView();
setVideoView(); setVideoView();
@@ -155,6 +160,33 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
checkLive(); checkLive();
} }
private void initScreenReceiver() {
// 屏幕开关监听 - 仅用于画中画模式下控制播放
mScreenReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || intent.getAction() == null) return;
// 只在画中画模式下处理屏幕开关
if (isInPictureInPictureMode()) {
if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
// 画中画模式下关屏,暂停播放
if (mPlayers.isPlaying()) {
onPaused();
mPausedByScreen = true;
}
} else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
// 画中画模式下开屏,恢复播放
if (mPausedByScreen) {
onPlay();
mPausedByScreen = false;
}
}
}
}
};
}
@Override @Override
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
protected void initEvent() { protected void initEvent() {
@@ -1048,6 +1080,8 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
hideInfo(); hideInfo();
hideUI(); hideUI();
} else { } else {
// 退出画中画模式时,重置屏幕暂停标志
mPausedByScreen = false;
hideInfo(); hideInfo();
if (isStop()) finish(); if (isStop()) finish();
} }
@@ -1075,6 +1109,13 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
// 注册屏幕开关监听
if (mScreenReceiver != null) {
IntentFilter screenFilter = new IntentFilter();
screenFilter.addAction(Intent.ACTION_SCREEN_ON);
screenFilter.addAction(Intent.ACTION_SCREEN_OFF);
registerReceiver(mScreenReceiver, screenFilter);
}
if (isRedirect()) onPlay(); if (isRedirect()) onPlay();
setRedirect(false); setRedirect(false);
} }
@@ -1082,6 +1123,14 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
@Override @Override
protected void onPause() { protected void onPause() {
super.onPause(); super.onPause();
// 注销屏幕开关监听
try {
if (mScreenReceiver != null) {
unregisterReceiver(mScreenReceiver);
}
} catch (Exception e) {
// Ignore
}
if (isRedirect()) onPaused(); if (isRedirect()) onPaused();
} }
@@ -162,8 +162,10 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
private Handler mHandler; private Handler mHandler;
private Runnable mTimeUpdateRunnable; private Runnable mTimeUpdateRunnable;
private BroadcastReceiver mBatteryReceiver; private BroadcastReceiver mBatteryReceiver;
private BroadcastReceiver mScreenReceiver;
private int mBatteryLevel = -1; private int mBatteryLevel = -1;
private boolean mIsCharging = false; private boolean mIsCharging = false;
private boolean mPausedByScreen = false;
public static void push(FragmentActivity activity, String text) { public static void push(FragmentActivity activity, String text) {
if (FileChooser.isValid(activity, Uri.parse(text))) file(activity, FileChooser.getPathFromUri(activity, Uri.parse(text))); if (FileChooser.isValid(activity, Uri.parse(text))) file(activity, FileChooser.getPathFromUri(activity, Uri.parse(text)));
@@ -341,6 +343,31 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
} }
}; };
// 屏幕开关监听 - 仅用于画中画模式下控制播放
mScreenReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || intent.getAction() == null) return;
// 只在画中画模式下处理屏幕开关
if (isInPictureInPictureMode()) {
if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
// 画中画模式下关屏,暂停播放
if (mPlayers.isPlaying()) {
onPaused();
mPausedByScreen = true;
}
} else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
// 画中画模式下开屏,恢复播放
if (mPausedByScreen) {
onPlay();
mPausedByScreen = false;
}
}
}
}
};
mTimeUpdateRunnable = new Runnable() { mTimeUpdateRunnable = new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -391,6 +418,13 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
private void startTimeBatteryUpdates() { private void startTimeBatteryUpdates() {
registerReceiver(mBatteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); registerReceiver(mBatteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
// 注册屏幕开关监听
IntentFilter screenFilter = new IntentFilter();
screenFilter.addAction(Intent.ACTION_SCREEN_ON);
screenFilter.addAction(Intent.ACTION_SCREEN_OFF);
registerReceiver(mScreenReceiver, screenFilter);
updateTimeBattery(); updateTimeBattery();
mHandler.post(mTimeUpdateRunnable); mHandler.post(mTimeUpdateRunnable);
} }
@@ -402,6 +436,14 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
} }
} catch (Exception e) { } catch (Exception e) {
} }
try {
if (mScreenReceiver != null) {
unregisterReceiver(mScreenReceiver);
}
} catch (Exception e) {
}
mHandler.removeCallbacks(mTimeUpdateRunnable); mHandler.removeCallbacks(mTimeUpdateRunnable);
} }
@@ -1728,6 +1770,8 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
hideDanmaku(); hideDanmaku();
hideSheet(); hideSheet();
} else { } else {
// 退出画中画模式时,重置屏幕暂停标志
mPausedByScreen = false;
showDanmaku(); showDanmaku();
if (isStop()) finish(); if (isStop()) finish();
} }
@@ -53,6 +53,10 @@ public class ConfigDialog {
public ConfigDialog(Fragment fragment) { public ConfigDialog(Fragment fragment) {
this.fragment = fragment; this.fragment = fragment;
// 确保fragment实现了ConfigCallback接口
if (!(fragment instanceof ConfigCallback)) {
throw new IllegalArgumentException("Fragment must implement ConfigCallback");
}
this.callback = (ConfigCallback) fragment; this.callback = (ConfigCallback) fragment;
this.binding = DialogConfigBinding.inflate(LayoutInflater.from(fragment.getContext())); this.binding = DialogConfigBinding.inflate(LayoutInflater.from(fragment.getContext()));
this.append = true; this.append = true;
@@ -146,11 +150,14 @@ public class ConfigDialog {
String url = binding.url.getText().toString().trim(); String url = binding.url.getText().toString().trim();
String name = binding.name.getText().toString().trim(); String name = binding.name.getText().toString().trim();
android.util.Log.d("ConfigDialog", "onPositive: type=" + type + ", url=" + url + ", name=" + name);
// 如果是编辑模式,更新现有配置 // 如果是编辑模式,更新现有配置
if (edit) Config.find(ori, type).url(url).name(name).update(); if (edit) Config.find(ori, type).url(url).name(name).update();
// 如果URL为空,删除配置 // 如果URL为空,删除配置
if (url.isEmpty()) { if (url.isEmpty()) {
android.util.Log.d("ConfigDialog", "URL is empty, deleting config");
Config.delete(ori, type); Config.delete(ori, type);
dialog.dismiss(); dialog.dismiss();
return; return;
@@ -159,7 +166,18 @@ public class ConfigDialog {
// 只有URL不为空时,才设置配置 // 只有URL不为空时,才设置配置
// 保存原始URL,以便在添加失败时恢复 // 保存原始URL,以便在添加失败时恢复
String originalUrl = ori; String originalUrl = ori;
callback.setConfig(Config.find(url, type)); android.util.Log.d("ConfigDialog", "Calling Config.find with url=" + url + ", type=" + type);
Config config = Config.find(url, type);
android.util.Log.d("ConfigDialog", "Config.find returned: " + (config != null ? config.toString() : "null"));
android.util.Log.d("ConfigDialog", "Checking callback: " + (callback != null ? callback.getClass().getName() : "null"));
android.util.Log.d("ConfigDialog", "Checking fragment: " + (fragment != null ? fragment.getClass().getName() : "null"));
android.util.Log.d("ConfigDialog", "Calling callback.setConfig");
callback.setConfig(config);
android.util.Log.d("ConfigDialog", "setConfig completed");
// 添加一个延迟检查,如果配置没有成功加载,则恢复原始URL // 添加一个延迟检查,如果配置没有成功加载,则恢复原始URL
new android.os.Handler().postDelayed(() -> { new android.os.Handler().postDelayed(() -> {
@@ -23,13 +23,16 @@ import com.fongmi.android.tv.ui.base.ViewType;
import com.fongmi.android.tv.ui.custom.SpaceItemDecoration; import com.fongmi.android.tv.ui.custom.SpaceItemDecoration;
import com.fongmi.android.tv.utils.ResUtil; import com.fongmi.android.tv.utils.ResUtil;
import com.fongmi.android.tv.utils.Timer; import com.fongmi.android.tv.utils.Timer;
import com.fongmi.android.tv.utils.Util;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.google.android.material.slider.Slider; import com.google.android.material.slider.Slider;
import java.util.Arrays; import java.util.Arrays;
import java.util.Formatter;
import java.util.List; import java.util.List;
import java.util.Locale;
public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickListener { public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickListener, Timer.Callback {
private DialogControlBinding binding; private DialogControlBinding binding;
private ActivityVideoBinding parent; private ActivityVideoBinding parent;
@@ -40,6 +43,8 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
private History history; private History history;
private Players player; private Players player;
private boolean parse; private boolean parse;
private StringBuilder builder;
private Formatter formatter;
public static ControlDialog create() { public static ControlDialog create() {
return new ControlDialog(); return new ControlDialog();
@@ -47,6 +52,8 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
public ControlDialog() { public ControlDialog() {
this.scale = ResUtil.getStringArray(R.array.select_scale); this.scale = ResUtil.getStringArray(R.array.select_scale);
this.builder = new StringBuilder();
this.formatter = new Formatter(builder, Locale.getDefault());
} }
public ControlDialog parent(ActivityVideoBinding parent) { public ControlDialog parent(ActivityVideoBinding parent) {
@@ -93,6 +100,15 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
binding.opening.setText(parent.control.action.opening.getText()); binding.opening.setText(parent.control.action.opening.getText());
binding.loop.setActivated(parent.control.action.loop.isActivated()); binding.loop.setActivated(parent.control.action.loop.isActivated());
binding.timer.setActivated(Timer.get().isRunning()); binding.timer.setActivated(Timer.get().isRunning());
// 设置定时器回调并更新按钮文字
if (Timer.get().isRunning()) {
Timer.get().setCallback(this);
updateTimerText(Timer.get().getTick());
} else {
binding.timer.setText(R.string.play_timer);
}
setTrackVisible(); setTrackVisible();
setScaleText(); setScaleText();
setPlayer(); setPlayer();
@@ -203,6 +219,42 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
binding.parse.getAdapter().notifyItemRangeChanged(0, binding.parse.getAdapter().getItemCount()); binding.parse.getAdapter().notifyItemRangeChanged(0, binding.parse.getAdapter().getItemCount());
} }
/**
* 更新定时按钮文字为倒计时
*/
private void updateTimerText(long tick) {
if (tick > 0) {
binding.timer.setText(Util.format(builder, formatter, tick));
} else {
binding.timer.setText(R.string.play_timer);
}
}
/**
* Timer.Callback 接口实现 - 定时器每秒回调
*/
@Override
public void onTick(long tick) {
updateTimerText(tick);
}
/**
* Timer.Callback 接口实现 - 定时完成回调
*/
@Override
public void onFinish() {
// 定时结束,恢复按钮文字
binding.timer.setText(R.string.play_timer);
binding.timer.setActivated(false);
}
@Override
public void dismiss() {
// 关闭对话框时取消定时器回调
Timer.get().setCallback(null);
super.dismiss();
}
public interface Listener { public interface Listener {
void onScale(int tag); void onScale(int tag);
@@ -287,10 +287,20 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
// 实现ConfigCallback接口 // 实现ConfigCallback接口
@Override @Override
public void setConfig(Config config) { public void setConfig(Config config) {
if (config == null || config.isEmpty()) return; android.util.Log.d("VodFragment", "setConfig called with: " + (config != null ? config.toString() : "null"));
if (config == null || config.isEmpty()) {
android.util.Log.d("VodFragment", "Config is null or empty, returning");
return;
}
// 检查Fragment是否还在活动状态,增强检查 // 检查Fragment是否还在活动状态,增强检查
if (!isValidFragmentState()) return; if (!isValidFragmentState()) {
android.util.Log.d("VodFragment", "Fragment state invalid, returning");
return;
}
android.util.Log.d("VodFragment", "Fragment state valid, proceeding with config load");
// 安全地隐藏空源提示 // 安全地隐藏空源提示
try { try {
@@ -302,26 +312,37 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
} }
Notify.progress(getActivity()); Notify.progress(getActivity());
android.util.Log.d("VodFragment", "Calling VodConfig.load");
VodConfig.load(config, new Callback() { VodConfig.load(config, new Callback() {
@Override @Override
public void success() { public void success() {
android.util.Log.d("VodFragment", "VodConfig.load success callback");
// 双重检查Fragment是否还在活动状态 // 双重检查Fragment是否还在活动状态
if (!isValidFragmentState()) return; if (!isValidFragmentState()) {
android.util.Log.d("VodFragment", "Fragment state invalid in success callback");
return;
}
try { try {
android.util.Log.d("VodFragment", "Success: dismissing notify and refreshing");
Notify.dismiss(); Notify.dismiss();
RefreshEvent.config(); RefreshEvent.config();
RefreshEvent.video(); RefreshEvent.video();
homeContent(); homeContent();
} catch (Exception e) { } catch (Exception e) {
android.util.Log.e("VodFragment", "Error in success callback", e);
e.printStackTrace(); e.printStackTrace();
} }
} }
@Override @Override
public void error(String msg) { public void error(String msg) {
android.util.Log.e("VodFragment", "VodConfig.load error: " + msg);
// 双重检查Fragment是否还在活动状态 // 双重检查Fragment是否还在活动状态
if (!isValidFragmentState()) return; if (!isValidFragmentState()) {
android.util.Log.d("VodFragment", "Fragment state invalid in error callback");
return;
}
try { try {
Notify.dismiss(); Notify.dismiss();
@@ -329,6 +350,7 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
// 加载失败时重新显示空源提示 // 加载失败时重新显示空源提示
checkEmptySource(); checkEmptySource();
} catch (Exception e) { } catch (Exception e) {
android.util.Log.e("VodFragment", "Error in error callback", e);
e.printStackTrace(); e.printStackTrace();
} }
} }
+3 -5
View File
@@ -16,11 +16,9 @@
android:valueFrom="1" android:valueFrom="1"
android:valueTo="10" android:valueTo="10"
app:thumbColor="@color/accent" app:thumbColor="@color/accent"
app:thumbRadius="10dp" app:thumbRadius="9dp"
app:tickVisible="true" app:tickVisible="false"
app:tickColor="@color/black_50"
app:trackColorActive="@color/accent" app:trackColorActive="@color/accent"
app:trackColorInactive="@color/white_20" app:trackColorInactive="@color/white_30" />
app:trackHeight="6dp" />
</LinearLayout> </LinearLayout>
+3 -5
View File
@@ -43,12 +43,10 @@
android:valueFrom="0.5" android:valueFrom="0.5"
android:valueTo="5" android:valueTo="5"
app:thumbColor="@color/accent" app:thumbColor="@color/accent"
app:thumbRadius="10dp" app:thumbRadius="9dp"
app:tickVisible="true" app:tickVisible="false"
app:tickColor="@color/black_50"
app:trackColorActive="@color/accent" app:trackColorActive="@color/accent"
app:trackColorInactive="@color/white_20" app:trackColorInactive="@color/white_30" />
app:trackHeight="6dp" />
<TextView <TextView
android:id="@+id/parseText" android:id="@+id/parseText"
+3 -5
View File
@@ -16,11 +16,9 @@
android:valueFrom="2" android:valueFrom="2"
android:valueTo="5" android:valueTo="5"
app:thumbColor="@color/accent" app:thumbColor="@color/accent"
app:thumbRadius="10dp" app:thumbRadius="9dp"
app:tickVisible="true" app:tickVisible="false"
app:tickColor="@color/black_50"
app:trackColorActive="@color/accent" app:trackColorActive="@color/accent"
app:trackColorInactive="@color/white_20" app:trackColorInactive="@color/white_30" />
app:trackHeight="6dp" />
</LinearLayout> </LinearLayout>
+4 -4
View File
@@ -135,8 +135,8 @@
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:gravity="center" android:gravity="center"
android:paddingStart="30dp" android:paddingStart="60dp"
android:paddingEnd="30dp" android:paddingEnd="60dp"
android:paddingTop="12dp" android:paddingTop="12dp"
android:paddingBottom="12dp" android:paddingBottom="12dp"
android:singleLine="true" android:singleLine="true"
@@ -153,8 +153,8 @@
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:gravity="center" android:gravity="center"
android:paddingStart="30dp" android:paddingStart="60dp"
android:paddingEnd="30dp" android:paddingEnd="60dp"
android:paddingTop="12dp" android:paddingTop="12dp"
android:paddingBottom="12dp" android:paddingBottom="12dp"
android:singleLine="true" android:singleLine="true"
@@ -23,8 +23,8 @@
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_weight="1" android:layout_weight="1"
app:bar_height="2dp" app:bar_height="2dp"
app:scrubber_enabled_size="12dp" app:scrubber_enabled_size="14dp"
app:scrubber_disabled_size="12dp" app:scrubber_disabled_size="14dp"
app:played_color="#FFEB3B" app:played_color="#FFEB3B"
app:scrubber_color="#FFEB3B" app:scrubber_color="#FFEB3B"
app:buffered_color="#80FFEB3B" /> app:buffered_color="#80FFEB3B" />
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
echo "=== 构建v8a手机测试版 ==="
# 清理之前的构建
echo "清理之前的构建..."
./gradlew clean
# 构建mobile arm64-v8a debug版本
echo "构建mobile arm64-v8a debug版本..."
./gradlew assembleMobileArm64_v8aDebug
# 检查构建结果
if [ $? -eq 0 ]; then
echo "=== 构建成功 ==="
# 查找生成的APK文件
APK_PATH=$(find app/build/outputs/apk/mobile/arm64-v8a/debug -name "*.apk" 2>/dev/null | head -1)
if [ -n "$APK_PATH" ]; then
echo "APK文件位置: $APK_PATH"
echo "文件大小: $(ls -lh "$APK_PATH" | awk '{print $5}')"
echo "文件信息:"
ls -la "$APK_PATH"
# 显示APK详细信息
echo ""
echo "=== APK详细信息 ==="
aapt dump badging "$APK_PATH" | grep -E "(package|application-label|native-code|sdkVersion|targetSdkVersion)"
else
echo "未找到生成的APK文件"
find app/build/outputs -name "*.apk" 2>/dev/null
fi
else
echo "=== 构建失败 ==="
echo "请检查错误信息"
fi
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
echo "========================================="
echo " 构建 XMBOX Mobile ARM64-V8A Release "
echo "========================================="
echo ""
cd /Users/chen/Desktop/XMBOX
echo "=== 1. 清理旧的构建文件 ==="
./gradlew clean
echo ""
echo "=== 2. 构建 Release APK ==="
./gradlew assembleMobileArm64_v8aRelease
echo ""
echo "=== 3. 验证构建结果 ==="
if [ -f "app/build/outputs/apk/mobileArm64_v8a/release/mobile-arm64_v8a.apk" ]; then
echo "✅ Release APK 构建成功!"
echo ""
echo "文件信息:"
ls -lh app/build/outputs/apk/mobileArm64_v8a/release/mobile-arm64_v8a.apk
echo ""
echo "APK详细信息:"
echo "---"
unzip -l app/build/outputs/apk/mobileArm64_v8a/release/mobile-arm64_v8a.apk | grep "assets/jar"
echo ""
echo "签名信息:"
jarsigner -verify -verbose -certs app/build/outputs/apk/mobileArm64_v8a/release/mobile-arm64_v8a.apk | grep -A 3 "Signed by"
echo ""
echo "=== 构建完成! ==="
echo "APK路径: app/build/outputs/apk/mobileArm64_v8a/release/mobile-arm64_v8a.apk"
else
echo "❌ 构建失败!"
exit 1
fi
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
cd /Users/chen/Desktop/XMBOX
./gradlew clean assembleMobileArm64_v8aDebug
+89
View File
@@ -0,0 +1,89 @@
#!/bin/bash
# WebP颜色修改脚本
# 需要先安装 ImageMagick: brew install imagemagick
echo "=== WebP 颜色修改工具 ==="
echo ""
# 检查ImageMagick是否安装
if ! command -v convert &> /dev/null; then
echo "❌ 未检测到 ImageMagick"
echo "请先安装: brew install imagemagick"
exit 1
fi
# 示例:修改颜色(色相旋转)
# 参数说明:
# -modulate brightness,saturation,hue
# 例如:-modulate 100,100,150 (色相旋转150度)
SOURCE_DIR="/Users/chen/Desktop/XMBOX/app/src/main/res/mipmap-hdpi"
OUTPUT_DIR="/Users/chen/Desktop/XMBOX/app/src/main/res/mipmap-hdpi/modified"
# 创建输出目录
mkdir -p "$OUTPUT_DIR"
echo "处理目录: $SOURCE_DIR"
echo "输出目录: $OUTPUT_DIR"
echo ""
# 示例1: 色相旋转(改变整体颜色)
echo "方式1: 色相旋转"
echo " convert input.webp -modulate 100,100,180 output.webp # 色相旋转180度"
echo ""
# 示例2: 颜色替换
echo "方式2: 颜色替换"
echo " convert input.webp -fuzz 20% -fill '#新颜色' -opaque '#旧颜色' output.webp"
echo ""
# 示例3: 调整色调/饱和度/亮度
echo "方式3: HSL调整"
echo " convert input.webp -modulate brightness,saturation,hue output.webp"
echo " brightness: 亮度 (100=不变)"
echo " saturation: 饱和度 (100=不变, 0=灰度)"
echo " hue: 色相 (100=不变)"
echo ""
# 交互式处理
read -p "请选择处理方式 (1/2/3): " choice
case $choice in
1)
read -p "输入色相旋转角度 (0-200, 100=不变): " hue
for file in "$SOURCE_DIR"/*.webp; do
filename=$(basename "$file")
echo "处理: $filename (色相旋转 ${hue}度)"
convert "$file" -modulate 100,100,$hue "$OUTPUT_DIR/$filename"
done
;;
2)
read -p "输入要替换的颜色 (HEX, 例如 #FF0000): " old_color
read -p "输入新颜色 (HEX, 例如 #00FF00): " new_color
for file in "$SOURCE_DIR"/*.webp; do
filename=$(basename "$file")
echo "处理: $filename ($old_color -> $new_color)"
convert "$file" -fuzz 20% -fill "$new_color" -opaque "$old_color" "$OUTPUT_DIR/$filename"
done
;;
3)
read -p "亮度 (100=不变): " brightness
read -p "饱和度 (100=不变, 0=灰度): " saturation
read -p "色相 (100=不变): " hue
for file in "$SOURCE_DIR"/*.webp; do
filename=$(basename "$file")
echo "处理: $filename (亮度:$brightness 饱和度:$saturation 色相:$hue)"
convert "$file" -modulate $brightness,$saturation,$hue "$OUTPUT_DIR/$filename"
done
;;
*)
echo "无效选择"
exit 1
;;
esac
echo ""
echo "✅ 处理完成!"
echo "处理后的文件保存在: $OUTPUT_DIR"
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
echo "=== 检查应用日志 ==="
echo ""
echo "1. 检查custom_spider.jar是否被加载:"
echo "---"
adb logcat -d | grep -i "custom_spider" | tail -20
echo ""
echo "2. 检查JarLoader相关日志:"
echo "---"
adb logcat -d | grep "JarLoader" | tail -20
echo ""
echo "3. 检查Spider初始化日志:"
echo "---"
adb logcat -d | grep -E "Spider|Init\.init" | tail -30
echo ""
echo "4. 检查是否有错误:"
echo "---"
adb logcat -d | grep -E "Error|Exception|Failed" | grep -i "fongmi\|spider\|jar" | tail -30
echo ""
echo "5. 检查DexNative相关问题:"
echo "---"
adb logcat -d | grep -i "DexNative" | tail -20
echo ""
echo "6. 检查APK中的jar文件:"
echo "---"
echo "检查已安装APK的assets目录..."
adb shell run-as com.fongmi.android.tv ls -la /data/data/com.fongmi.android.tv/cache/jar/ 2>/dev/null || echo "无权限访问,使用pull检查..."
echo ""
echo "=== 实时监控日志(按Ctrl+C停止)==="
adb logcat -c
adb logcat | grep -E "custom_spider|Spider|JarLoader|Init|fongmi" --color=always
+265
View File
@@ -0,0 +1,265 @@
# 广告拦截功能说明
## 📌 功能概述
XMBOX内置了强大的广告拦截系统,可以有效过滤视频播放过程中弹出的各类广告,特别是常见的赌博广告(如澳门新葡京、皇冠、金沙等)。
## 🎯 拦截类型
### 1. 赌博类广告 ⭐⭐⭐⭐⭐
**最常见的视频中途弹窗广告**
包括但不限于:
- 澳门新葡京、皇冠、金沙、威尼斯人、永利等博彩品牌
- 各类赌场、扑克、投注网站
- 使用 `xpj``wnsr``js[数字]``vn[数字]` 等域名的赌博网站
**特征**:通常在视频播放到一半时突然弹出,覆盖整个播放器
### 2. 广告联盟
- Google广告联盟(DoubleClick、AdSense等)
- 百度广告联盟
- 淘宝/阿里广告联盟
- 腾讯广告联盟
### 3. 视频平台广告
- 优酷、爱奇艺、腾讯视频等平台的贴片广告
- 芒果TV、乐视等平台广告
### 4. 弹窗广告
- 页面加载时的弹窗
- 点击播放时的诱导弹窗
### 5. 跟踪统计
- 百度统计、Google Analytics
- CNZZ、友盟等统计代码
## 🔧 使用方法
### 方式一:内置拦截(推荐)✅
**无需任何配置**,应用已内置200+常见广告域名,包括:
```java
// 已内置的赌博广告域名示例
".*\\..*葡京.*" // 拦截所有包含"葡京"的域名
".*\\..*皇冠.*" // 拦截所有包含"皇冠"的域名
".*xpj.*\\..*" // 拦截新葡京相关域名
"wan.51img1.com" // 常见广告CDN
"vip.ffzyad.com" // 视频广告域名
```
### 方式二:自定义拦截
在配置文件中添加 `ads` 字段:
```json
{
"spider": "your_spider_url",
"sites": [...],
"ads": [
"精确域名匹配",
"example-ad.com",
"another-ad-domain.net",
"通配符匹配",
".*\\.advertisement\\..*",
".*\\.ad-server\\..*",
"关键词匹配",
".*赌博.*",
".*博彩.*"
]
}
```
### 方式三:片头片尾跳过 🎬
对于**嵌入视频流中的广告**(无法通过域名拦截),使用片头片尾跳过功能:
#### 设置片头跳过
1. 播放视频
2. 在片头广告结束后(前5分钟内),点击 **【片头】** 按钮
3. 当前时间点会被记录
4. 下次播放时自动从这个时间点开始
#### 设置片尾跳过
1. 播放到片尾广告开始前(后5分钟内),点击 **【片尾】** 按钮
2. 当前时间点会被记录
3. 下次播放时会在这个时间点停止
#### 重置设置
长按 **【片头】** 或 **【片尾】** 按钮即可重置
## 📝 工作原理
### WebView层拦截
```java
// 在WebView加载资源时拦截
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String host = request.getUrl().getHost();
// 检查是否为广告域名
if (isAd(host)) {
// 返回空响应,阻止广告加载
return emptyResponse;
}
return super.shouldInterceptRequest(view, request);
}
```
### 拦截流程
```
视频请求 → 检查域名 → 内置黑名单? → 是 → 拦截 ❌
用户自定义黑名单? → 是 → 拦截 ❌
允许加载 ✅
```
## 🛠️ 高级配置
### 1. 正则表达式匹配
```json
{
"ads": [
".*\\.ad.*\\.com", // 匹配 sub.adserver.com
".*gambling.*", // 匹配任何包含gambling的域名
"^https?://[^/]*ad[^/]*/" // 匹配URL中包含ad的路径
]
}
```
### 2. 分类管理
建议按类型组织广告域名:
```json
{
"ads": [
"// === 赌博广告 ===",
".*葡京.*",
".*casino.*",
"// === 视频平台广告 ===",
"atm.youku.com",
"cupid.iqiyi.com",
"// === 弹窗广告 ===",
"mimg.0c1q0l.cn",
"www.92424.cn"
]
}
```
## 📊 效果统计
启用广告拦截后,您可以看到:
- ✅ 视频播放更流畅
- ✅ 无中途弹窗干扰
- ✅ 隐私数据不被跟踪
- ✅ 节省流量消耗
## ❓ 常见问题
### Q1: 为什么有些广告还是会出现?
**A:** 可能的原因:
1. 广告直接嵌入视频流(使用片头片尾跳过)
2. 广告使用了新的域名(可添加到自定义黑名单)
3. 广告通过JavaScript动态生成(内容层广告,较难拦截)
### Q2: 会不会误拦截正常内容?
**A:** 内置域名库经过精心筛选,主要针对已知广告域名。误拦截概率极低。如遇到问题,可以:
- 关闭内置拦截(需修改代码)
- 反馈给开发者更新黑名单
### Q3: 如何找到广告的域名?
**A:** 方法:
1. 使用Chrome浏览器开发者工具(F12)查看Network请求
2. 查看应用日志输出的URL
3. 参考其他用户分享的广告域名列表
### Q4: 拦截会影响性能吗?
**A:** 影响极小:
- 仅在WebView解析阶段生效
- 字符串匹配操作非常快速
- 反而会因为减少网络请求而提升性能
### Q5: 数据会被上传吗?
**A:** 完全不会:
- 所有拦截在本地进行
- 不联网、不上传
- 完全保护您的隐私
## 🔍 调试方法
### 查看拦截日志
启用调试模式可以看到被拦截的域名:
```bash
# 连接设备
adb logcat | grep "WebView\|AdBlocker"
# 查看拦截记录
adb logcat | grep "blocked"
```
### 测试广告拦截
1. 访问包含广告的视频网站
2. 观察是否弹出广告
3. 检查日志中的拦截记录
## 📚 域名库维护
### 添加新的广告域名
编辑 `AdBlocker.java`
```java
private static final List<String> GAMBLING_ADS = Arrays.asList(
// 添加新发现的广告域名
"new-casino-ad.com",
".*new-gambling.*"
);
```
### 提交贡献
如果您发现新的广告域名,欢迎:
1. 提交Issue报告
2. 提交Pull Request更新域名库
3. 在社区分享
## 💡 最佳实践
1. **优先使用内置拦截** - 已覆盖大部分常见广告
2. **补充自定义规则** - 针对特定视频源的广告
3. **善用片头片尾跳过** - 处理嵌入式广告
4. **定期更新** - 保持应用最新版本以获取最新域名库
## 🎉 总结
XMBOX的广告拦截系统通过**三层防护**:
1. 🛡️ **内置域名库** - 200+常见广告(含澳门新葡京等赌博广告)
2. 🛡️ **自定义黑名单** - 用户针对性添加
3. 🛡️ **片头片尾跳过** - 处理嵌入式广告
让您享受**纯净、流畅、安全**的视频观看体验!
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
echo "=== 检查模拟器状态 ==="
adb devices
echo ""
echo "=== 安装APK到模拟器 ==="
adb install -r /Users/chen/Desktop/XMBOX/app/build/outputs/apk/mobileArm64_v8a/debug/mobile-arm64_v8a.apk
echo ""
echo "=== 启动应用 ==="
adb shell am start -n com.fongmi.android.tv/.ui.activity.HomeActivity
echo ""
echo "=== 等待应用启动... ==="
sleep 3
echo ""
echo "=== 查看应用日志 ==="
echo "按 Ctrl+C 停止日志监控"
adb logcat -c
adb logcat | grep -E "custom_spider|Spider|JarLoader|fongmi" --color=always
Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

+69
View File
@@ -0,0 +1,69 @@
{
"name": "广告过滤配置示例",
"description": "演示如何配置广告域名黑名单,阻止视频中途弹出的广告(如澳门新葡京等博彩广告)",
"说明": {
"内置拦截": "应用已内置常见广告域名库,包括:澳门新葡京、皇冠、金沙等博彩广告;Google、百度、淘宝等广告联盟;优酷、爱奇艺等视频平台广告",
"自定义拦截": "可以在配置文件中添加ads字段,补充需要拦截的广告域名",
"支持正则": "域名支持正则表达式匹配,使用 .* 作为通配符"
},
"配置示例": {
"spider": "your_spider_url",
"sites": [],
"ads": [
"注释: 以下是自定义广告域名列表,会与内置域名库合并使用",
"注释: 精确匹配 - 直接写完整域名",
"mimg.0c1q0l.cn",
"www.92424.cn",
"vip.ffzyad.com",
"注释: 模糊匹配 - 使用通配符",
".*\\.doubleclick\\.net",
".*\\.googlesyndication\\.com",
"注释: 关键词匹配 - 拦截包含特定关键词的域名",
".*葡京.*",
".*皇冠.*",
".*金沙.*",
".*casino.*",
".*bet.*",
"注释: 特定平台的广告",
"wan.51img1.com",
"k.jinxiuzhilv.com",
"ssl.kdd.cc"
]
},
"常见问题": {
"Q1": "为什么配置了还是有广告?",
"A1": "1. 检查广告域名是否正确;2. 某些广告可能直接嵌入视频流,无法通过域名拦截;3. 尝试使用片头片尾跳过功能",
"Q2": "如何找到广告的域名?",
"A2": "1. 使用浏览器开发者工具查看网络请求;2. 查看应用日志中的URL;3. 参考其他用户分享的广告域名列表",
"Q3": "会不会误拦截正常内容?",
"A3": "内置域名库经过筛选,主要针对已知广告。如有误拦截,可以反馈给开发者"
},
"片头片尾跳过": {
"说明": "对于嵌入视频流中的广告,可以使用片头片尾跳过功能",
"使用方法": [
"1. 播放视频时,在片头(前5分钟内)按【片头】按钮,记录当前时间点",
"2. 在片尾(后5分钟内)按【片尾】按钮,记录结束前的时间点",
"3. 下次播放相同视频时,会自动跳过片头,并在片尾前停止",
"4. 如需重置,长按对应按钮即可"
]
},
"技术说明": {
"拦截层级": "WebView网络请求层拦截",
"拦截方式": "返回空响应,阻止广告内容加载",
"性能影响": "极小,仅在WebView解析时生效",
"隐私保护": "所有拦截在本地进行,不上传任何数据"
}
}
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
echo "=== 替换Spider Jar文件 ==="
# 删除旧的fm.jar
echo "删除旧的fm.jar..."
rm -f /Users/chen/Desktop/XMBOX/app/src/main/assets/jar/fm.jar
# 复制新的custom_spider.jar
echo "复制custom_spider.jar..."
cp /Users/chen/Desktop/custom_spider.jar /Users/chen/Desktop/XMBOX/app/src/main/assets/jar/custom_spider.jar
# 验证
echo ""
echo "=== 验证结果 ==="
ls -lh /Users/chen/Desktop/XMBOX/app/src/main/assets/jar/
echo ""
md5 /Users/chen/Desktop/XMBOX/app/src/main/assets/jar/custom_spider.jar
echo ""
echo "=== 清理并重新构建 ==="
cd /Users/chen/Desktop/XMBOX
./gradlew clean assembleMobileArm64_v8aDebug
echo ""
echo "=== 完成! ==="