feat: 升级到v3.1.0
- 实现定时按钮倒计时显示功能 - 适配pixel主题化图标展示 - 优化TimerDialog按钮宽度设计
@@ -27,8 +27,8 @@ android {
|
||||
minSdk 24
|
||||
//noinspection ExpiredTargetSdkVersion
|
||||
targetSdk 28
|
||||
versionCode 309
|
||||
versionName "3.0.9"
|
||||
versionCode 310
|
||||
versionName "3.1.0"
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
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.thegrizzlylabs:sardine-android:0.9'
|
||||
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.guolindev.permissionx:permissionx:1.7.1'
|
||||
implementation 'com.hierynomus:smbj:0.13.0'
|
||||
|
||||
@@ -14,11 +14,9 @@
|
||||
android:valueFrom="1"
|
||||
android:valueTo="10"
|
||||
app:thumbColor="@color/primary"
|
||||
app:thumbRadius="10dp"
|
||||
app:tickVisible="true"
|
||||
app:tickColor="@color/black_50"
|
||||
app:thumbRadius="9dp"
|
||||
app:tickVisible="false"
|
||||
app:trackColorActive="@color/primary"
|
||||
app:trackColorInactive="@color/white_20"
|
||||
app:trackHeight="4dp" />
|
||||
app:trackColorInactive="@color/white_30" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -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) {
|
||||
android.util.Log.d("VodConfig", "load called with config: " + (config != null ? config.toString() : "null"));
|
||||
|
||||
// 参数检查
|
||||
if (config == null || callback == null) {
|
||||
android.util.Log.e("VodConfig", "Invalid parameters: config=" + config + ", callback=" + callback);
|
||||
if (callback != null) {
|
||||
App.post(() -> callback.error("配置参数无效"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
android.util.Log.d("VodConfig", "Parameters valid, proceeding with load");
|
||||
|
||||
// 添加加载状态检查,防止并发加载
|
||||
VodConfig instance = get();
|
||||
synchronized (instance) {
|
||||
if (instance.isLoading) {
|
||||
android.util.Log.d("VodConfig", "Already loading, cancelling previous load");
|
||||
// 如果正在加载,取消之前的加载
|
||||
try {
|
||||
OkHttp.cancel("vod");
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("VodConfig", "Error cancelling previous load", e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
instance.isLoading = true;
|
||||
}
|
||||
|
||||
android.util.Log.d("VodConfig", "Calling instance.clear().config(config).load(callback)");
|
||||
try {
|
||||
instance.clear().config(config).load(callback);
|
||||
} catch (Exception e) {
|
||||
instance.isLoading = false;
|
||||
android.util.Log.e("VodConfig", "Exception during load", e);
|
||||
e.printStackTrace();
|
||||
App.post(() -> callback.error("配置加载失败: " + e.getMessage()));
|
||||
}
|
||||
@@ -224,14 +233,26 @@ public class VodConfig {
|
||||
return;
|
||||
}
|
||||
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")) {
|
||||
Site site = Site.objectFrom(element);
|
||||
if (sites.contains(site)) continue;
|
||||
site.setApi(UrlUtil.convert(site.getApi()));
|
||||
site.setExt(UrlUtil.convert(site.getExt()));
|
||||
site.setJar(parseJar(site, spider));
|
||||
sites.add(site.trans().sync());
|
||||
try {
|
||||
Site site = Site.objectFrom(element);
|
||||
if (sites.contains(site)) continue;
|
||||
site.setApi(UrlUtil.convert(site.getApi()));
|
||||
site.setExt(UrlUtil.convert(site.getExt()));
|
||||
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) {
|
||||
if (site.getKey().equals(config.getHome())) {
|
||||
|
||||
@@ -46,10 +46,15 @@ public class JarLoader {
|
||||
}
|
||||
|
||||
private void load(String key, File file) {
|
||||
if (!file.setReadOnly()) return;
|
||||
loaders.put(key, dex(file));
|
||||
invokeInit(key);
|
||||
putProxy(key);
|
||||
try {
|
||||
if (!file.setReadOnly()) return;
|
||||
loaders.put(key, dex(file));
|
||||
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) {
|
||||
@@ -85,19 +90,24 @@ public class JarLoader {
|
||||
}
|
||||
|
||||
public synchronized void parseJar(String key, String jar) {
|
||||
if (loaders.containsKey(key)) return;
|
||||
String[] texts = jar.split(";md5;");
|
||||
String md5 = texts.length > 1 ? texts[1].trim() : "";
|
||||
if (md5.startsWith("http")) md5 = OkHttp.string(md5).trim();
|
||||
jar = texts[0];
|
||||
if (!md5.isEmpty() && Util.equals(jar, md5)) {
|
||||
load(key, Path.jar(jar));
|
||||
} else if (jar.startsWith("http")) {
|
||||
load(key, download(jar));
|
||||
} else if (jar.startsWith("file")) {
|
||||
load(key, Path.local(jar));
|
||||
} else if (jar.startsWith("assets")) {
|
||||
parseJar(key, UrlUtil.convert(jar));
|
||||
try {
|
||||
if (loaders.containsKey(key)) return;
|
||||
String[] texts = jar.split(";md5;");
|
||||
String md5 = texts.length > 1 ? texts[1].trim() : "";
|
||||
if (md5.startsWith("http")) md5 = OkHttp.string(md5).trim();
|
||||
jar = texts[0];
|
||||
if (!md5.isEmpty() && Util.equals(jar, md5)) {
|
||||
load(key, Path.jar(jar));
|
||||
} else if (jar.startsWith("http")) {
|
||||
load(key, download(jar));
|
||||
} else if (jar.startsWith("file")) {
|
||||
load(key, Path.local(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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.annotation.NonNull;
|
||||
import com.fongmi.android.tv.App;
|
||||
import com.fongmi.android.tv.Constant;
|
||||
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.VodConfig;
|
||||
import com.fongmi.android.tv.impl.ParseCallback;
|
||||
@@ -177,8 +178,13 @@ public class CustomWebView extends WebView implements DialogInterface.OnDismissL
|
||||
}
|
||||
|
||||
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 : LiveConfig.get().getAds()) if (Util.containOrMatch(host, ad)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
android:topLeftRadius="8dp"
|
||||
android:topRightRadius="8dp" />
|
||||
|
||||
<solid android:color="#B32196F3" />
|
||||
<solid android:color="#B3000000" />
|
||||
|
||||
<padding
|
||||
android:bottom="4dp"
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/white" />
|
||||
<!-- 使用纯色背景,自动适配深浅色模式 -->
|
||||
<background android:drawable="@color/launcher_background" />
|
||||
<!-- 前景图标:使用 inset 缩小显示(因为图标铺满了画布)-->
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@mipmap/ic_launcher_foreground"
|
||||
android:inset="20%" />
|
||||
</foreground>
|
||||
<!-- 主题图标:也需要使用 inset 保持大小一致 -->
|
||||
<monochrome>
|
||||
<inset
|
||||
android:drawable="@mipmap/ic_launcher_foreground"
|
||||
android:inset="20%" />
|
||||
</monochrome>
|
||||
</adaptive-icon>
|
||||
@@ -1,9 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/white" />
|
||||
<!-- 使用纯色背景,自动适配深浅色模式 -->
|
||||
<background android:drawable="@color/launcher_background" />
|
||||
<!-- 前景图标:使用 inset 缩小显示(因为图标铺满了画布)-->
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@mipmap/ic_launcher_foreground"
|
||||
android:inset="20%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
<!-- 主题图标:也需要使用 inset 保持大小一致 -->
|
||||
<monochrome>
|
||||
<inset
|
||||
android:drawable="@mipmap/ic_launcher_foreground"
|
||||
android:inset="20%" />
|
||||
</monochrome>
|
||||
</adaptive-icon>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 998 B |
|
Before Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 998 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 830 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 830 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 830 B |
|
Before Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 830 B |
|
Before Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,7 @@
|
||||
<resources>
|
||||
|
||||
<!-- Launcher icon background (Dark mode) -->
|
||||
<color name="launcher_background">#222222</color>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
<string name="setting_app">应用设置</string>
|
||||
<string name="setting_network">网络设置</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>
|
||||
|
||||
<!-- Backup & Restore -->
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
<string name="setting_app">應用設置</string>
|
||||
<string name="setting_network">網絡設置</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>
|
||||
|
||||
<!-- Backup & Restore -->
|
||||
|
||||
@@ -27,5 +27,8 @@
|
||||
<color name="white_90">#E6FFFFFF</color>
|
||||
<color name="text_toast">#FFEB3B</color>
|
||||
<color name="yellow_500">#FFEB3B</color>
|
||||
|
||||
<!-- Launcher icon background (Light mode) -->
|
||||
<color name="launcher_background">#FFFFFF</color>
|
||||
|
||||
</resources>
|
||||
@@ -92,7 +92,7 @@
|
||||
<string name="setting_choose">Choose</string>
|
||||
<string name="setting_off">Off</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>
|
||||
|
||||
<!-- Backup & Restore -->
|
||||
@@ -152,7 +152,7 @@
|
||||
<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_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_search_empty">搜索无结果,换个关键词试试</string>
|
||||
<string name="error_detail">No play data</string>
|
||||
@@ -238,8 +238,8 @@
|
||||
<string name="target_size">Target size</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="add_source">Add Source</string>
|
||||
<string name="source_hint">空谷无音,待君添源</string>
|
||||
<string name="add_source">添加源</string>
|
||||
|
||||
<!-- 隐私协议相关 -->
|
||||
<string name="privacy_agreement_title">XMBOX软件许可协议</string>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.fongmi.android.tv.ui.activity;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.drawable.Drawable;
|
||||
@@ -98,6 +100,8 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
|
||||
private String tag;
|
||||
private int count;
|
||||
private PiP mPiP;
|
||||
private BroadcastReceiver mScreenReceiver;
|
||||
private boolean mPausedByScreen = false;
|
||||
|
||||
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));
|
||||
@@ -148,6 +152,7 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
|
||||
mR2 = this::setTraffic;
|
||||
mR3 = this::hideInfo;
|
||||
mPiP = new PiP();
|
||||
initScreenReceiver();
|
||||
Server.get().start();
|
||||
setRecyclerView();
|
||||
setVideoView();
|
||||
@@ -155,6 +160,33 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
|
||||
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
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
protected void initEvent() {
|
||||
@@ -1048,6 +1080,8 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
|
||||
hideInfo();
|
||||
hideUI();
|
||||
} else {
|
||||
// 退出画中画模式时,重置屏幕暂停标志
|
||||
mPausedByScreen = false;
|
||||
hideInfo();
|
||||
if (isStop()) finish();
|
||||
}
|
||||
@@ -1075,6 +1109,13 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
|
||||
@Override
|
||||
protected void 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();
|
||||
setRedirect(false);
|
||||
}
|
||||
@@ -1082,6 +1123,14 @@ public class LiveActivity extends BaseActivity implements CustomKeyDownLive.List
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
// 注销屏幕开关监听
|
||||
try {
|
||||
if (mScreenReceiver != null) {
|
||||
unregisterReceiver(mScreenReceiver);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore
|
||||
}
|
||||
if (isRedirect()) onPaused();
|
||||
}
|
||||
|
||||
|
||||
@@ -162,8 +162,10 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
|
||||
private Handler mHandler;
|
||||
private Runnable mTimeUpdateRunnable;
|
||||
private BroadcastReceiver mBatteryReceiver;
|
||||
private BroadcastReceiver mScreenReceiver;
|
||||
private int mBatteryLevel = -1;
|
||||
private boolean mIsCharging = false;
|
||||
private boolean mPausedByScreen = false;
|
||||
|
||||
public static void push(FragmentActivity activity, String 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() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -391,6 +418,13 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
|
||||
|
||||
private void startTimeBatteryUpdates() {
|
||||
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();
|
||||
mHandler.post(mTimeUpdateRunnable);
|
||||
}
|
||||
@@ -402,6 +436,14 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
try {
|
||||
if (mScreenReceiver != null) {
|
||||
unregisterReceiver(mScreenReceiver);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
mHandler.removeCallbacks(mTimeUpdateRunnable);
|
||||
}
|
||||
|
||||
@@ -1728,6 +1770,8 @@ public class VideoActivity extends BaseActivity implements Clock.Callback, Custo
|
||||
hideDanmaku();
|
||||
hideSheet();
|
||||
} else {
|
||||
// 退出画中画模式时,重置屏幕暂停标志
|
||||
mPausedByScreen = false;
|
||||
showDanmaku();
|
||||
if (isStop()) finish();
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@ public class ConfigDialog {
|
||||
|
||||
public ConfigDialog(Fragment fragment) {
|
||||
this.fragment = fragment;
|
||||
// 确保fragment实现了ConfigCallback接口
|
||||
if (!(fragment instanceof ConfigCallback)) {
|
||||
throw new IllegalArgumentException("Fragment must implement ConfigCallback");
|
||||
}
|
||||
this.callback = (ConfigCallback) fragment;
|
||||
this.binding = DialogConfigBinding.inflate(LayoutInflater.from(fragment.getContext()));
|
||||
this.append = true;
|
||||
@@ -146,11 +150,14 @@ public class ConfigDialog {
|
||||
String url = binding.url.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();
|
||||
|
||||
// 如果URL为空,删除配置
|
||||
if (url.isEmpty()) {
|
||||
android.util.Log.d("ConfigDialog", "URL is empty, deleting config");
|
||||
Config.delete(ori, type);
|
||||
dialog.dismiss();
|
||||
return;
|
||||
@@ -159,7 +166,18 @@ public class ConfigDialog {
|
||||
// 只有URL不为空时,才设置配置
|
||||
// 保存原始URL,以便在添加失败时恢复
|
||||
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
|
||||
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.utils.ResUtil;
|
||||
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.slider.Slider;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Formatter;
|
||||
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 ActivityVideoBinding parent;
|
||||
@@ -40,6 +43,8 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
|
||||
private History history;
|
||||
private Players player;
|
||||
private boolean parse;
|
||||
private StringBuilder builder;
|
||||
private Formatter formatter;
|
||||
|
||||
public static ControlDialog create() {
|
||||
return new ControlDialog();
|
||||
@@ -47,6 +52,8 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
|
||||
|
||||
public ControlDialog() {
|
||||
this.scale = ResUtil.getStringArray(R.array.select_scale);
|
||||
this.builder = new StringBuilder();
|
||||
this.formatter = new Formatter(builder, Locale.getDefault());
|
||||
}
|
||||
|
||||
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.loop.setActivated(parent.control.action.loop.isActivated());
|
||||
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();
|
||||
setScaleText();
|
||||
setPlayer();
|
||||
@@ -203,6 +219,42 @@ public class ControlDialog extends BaseDialog implements ParseAdapter.OnClickLis
|
||||
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 {
|
||||
|
||||
void onScale(int tag);
|
||||
|
||||
@@ -287,10 +287,20 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
|
||||
// 实现ConfigCallback接口
|
||||
@Override
|
||||
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是否还在活动状态,增强检查
|
||||
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 {
|
||||
@@ -302,26 +312,37 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
|
||||
}
|
||||
|
||||
Notify.progress(getActivity());
|
||||
android.util.Log.d("VodFragment", "Calling VodConfig.load");
|
||||
VodConfig.load(config, new Callback() {
|
||||
@Override
|
||||
public void success() {
|
||||
android.util.Log.d("VodFragment", "VodConfig.load success callback");
|
||||
// 双重检查Fragment是否还在活动状态
|
||||
if (!isValidFragmentState()) return;
|
||||
if (!isValidFragmentState()) {
|
||||
android.util.Log.d("VodFragment", "Fragment state invalid in success callback");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
android.util.Log.d("VodFragment", "Success: dismissing notify and refreshing");
|
||||
Notify.dismiss();
|
||||
RefreshEvent.config();
|
||||
RefreshEvent.video();
|
||||
homeContent();
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("VodFragment", "Error in success callback", e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(String msg) {
|
||||
android.util.Log.e("VodFragment", "VodConfig.load error: " + msg);
|
||||
// 双重检查Fragment是否还在活动状态
|
||||
if (!isValidFragmentState()) return;
|
||||
if (!isValidFragmentState()) {
|
||||
android.util.Log.d("VodFragment", "Fragment state invalid in error callback");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Notify.dismiss();
|
||||
@@ -329,6 +350,7 @@ public class VodFragment extends BaseFragment implements SiteCallback, FilterCal
|
||||
// 加载失败时重新显示空源提示
|
||||
checkEmptySource();
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("VodFragment", "Error in error callback", e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,9 @@
|
||||
android:valueFrom="1"
|
||||
android:valueTo="10"
|
||||
app:thumbColor="@color/accent"
|
||||
app:thumbRadius="10dp"
|
||||
app:tickVisible="true"
|
||||
app:tickColor="@color/black_50"
|
||||
app:thumbRadius="9dp"
|
||||
app:tickVisible="false"
|
||||
app:trackColorActive="@color/accent"
|
||||
app:trackColorInactive="@color/white_20"
|
||||
app:trackHeight="6dp" />
|
||||
app:trackColorInactive="@color/white_30" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -43,12 +43,10 @@
|
||||
android:valueFrom="0.5"
|
||||
android:valueTo="5"
|
||||
app:thumbColor="@color/accent"
|
||||
app:thumbRadius="10dp"
|
||||
app:tickVisible="true"
|
||||
app:tickColor="@color/black_50"
|
||||
app:thumbRadius="9dp"
|
||||
app:tickVisible="false"
|
||||
app:trackColorActive="@color/accent"
|
||||
app:trackColorInactive="@color/white_20"
|
||||
app:trackHeight="6dp" />
|
||||
app:trackColorInactive="@color/white_30" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/parseText"
|
||||
|
||||
@@ -16,11 +16,9 @@
|
||||
android:valueFrom="2"
|
||||
android:valueTo="5"
|
||||
app:thumbColor="@color/accent"
|
||||
app:thumbRadius="10dp"
|
||||
app:tickVisible="true"
|
||||
app:tickColor="@color/black_50"
|
||||
app:thumbRadius="9dp"
|
||||
app:tickVisible="false"
|
||||
app:trackColorActive="@color/accent"
|
||||
app:trackColorInactive="@color/white_20"
|
||||
app:trackHeight="6dp" />
|
||||
app:trackColorInactive="@color/white_30" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -135,8 +135,8 @@
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:paddingStart="30dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:paddingStart="60dp"
|
||||
android:paddingEnd="60dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:singleLine="true"
|
||||
@@ -153,8 +153,8 @@
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:paddingStart="30dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:paddingStart="60dp"
|
||||
android:paddingEnd="60dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:singleLine="true"
|
||||
|
||||