feat: 升级到v3.1.0

- 实现定时按钮倒计时显示功能
- 适配pixel主题化图标展示
- 优化TimerDialog按钮宽度设计
This commit is contained in:
您的名字
2025-10-28 19:40:13 +08:00
parent 794e1a32fe
commit 514368bc07
72 changed files with 1169 additions and 67 deletions
+3 -3
View File
@@ -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>
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) {
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;
}
+1 -1
View File
@@ -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>
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_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 -->
+1 -1
View File
@@ -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 -->
+3
View File
@@ -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>
+4 -4
View File
@@ -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();
}
}
+3 -5
View File
@@ -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>
+3 -5
View File
@@ -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"
+3 -5
View File
@@ -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>
+4 -4
View File
@@ -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"