Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0888f7930 | |||
| 3441bbc8f0 | |||
| 13bc801b12 | |||
| 9f3b631dfb | |||
| 8cfea9ef79 | |||
| 4a8a84a4eb | |||
| 56251db9e7 | |||
| 8357ebefcf | |||
| b16cb4d193 | |||
| df0333d26e | |||
| 0fd0e245d4 |
@@ -35,3 +35,24 @@ google-services.json
|
||||
|
||||
# APK files
|
||||
apk/release/*.apk
|
||||
|
||||
# Local configuration files
|
||||
gradle.properties.local
|
||||
local.properties
|
||||
local-repo/
|
||||
|
||||
# Temporary files
|
||||
cleanup_project.sh
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Duplicate release directories
|
||||
XMBOX-Release/
|
||||
|
||||
# Tools and utilities (not part of the project)
|
||||
other/tools/bfg.jar
|
||||
other/tools/cleaner.bat
|
||||
@@ -2,7 +2,7 @@
|
||||
</h1>
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
@@ -10,6 +10,7 @@
|
||||
一个操作方便、界面简洁的Android视频播放器盒子,需自行添源,支持TV和手机双平台。
|
||||
|
||||
[下载APK](https://github.com/Tosencen/XMBOX-Release/tree/main/apk/release) • [功能特性](#-功能特性) • [构建指南](#-构建指南) • [API文档](#-api文档)
|
||||
<img width="1920" height="864" alt="Group 15" src="https://github.com/user-attachments/assets/e69741bd-a21d-417e-ad85-e747032f6daf" />
|
||||
|
||||
</div>
|
||||
|
||||
@@ -36,16 +37,17 @@
|
||||
|
||||
## 📥 下载安装
|
||||
|
||||
### 最新版本: v3.0.8
|
||||
### 最新版本: v3.1.0
|
||||
|
||||
| 平台 | 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) |
|
||||
| **📺 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) |
|
||||
| **📱 手机版** | [下载 (34.4MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.1.0/mobile-arm64_v8a-v3.1.0.apk) | [下载 (30.4MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.1.0/mobile-armeabi_v7a-v3.1.0.apk) |
|
||||
| **📺 TV版** | [下载 (34.5MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.1.0/leanback-arm64_v8a-v3.1.0.apk) | [下载 (30.5MB)](https://github.com/Tosencen/XMBOX-Release/raw/main/apk/release/v3.1.0/leanback-armeabi_v7a-v3.1.0.apk) |
|
||||
|
||||
### 📁 版本历史
|
||||
- **v3.1.0**: [查看v3.1.0版本](https://github.com/Tosencen/XMBOX-Release/tree/main/apk/release/v3.1.0) - 定时器优化和画中画修复版本
|
||||
- **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.7**: [查看v3.0.7版本](https://github.com/Tosencen/XMBOX-Release/tree/main/apk/release/v3.0.7) - 全面优化稳定性和用户体验
|
||||
|
||||
### 📦 下载说明
|
||||
- **最新版本**: 根目录的 `mobile.json` 和 `leanback.json` 包含最新版本信息
|
||||
@@ -132,6 +134,36 @@ XMBOX/
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
### v3.1.0 (2025-10-28)
|
||||
|
||||
#### ✨ 新功能
|
||||
* **定时倒计时显示** - 实现定时按钮倒计时显示功能,用户可以实时看到剩余时间
|
||||
* **主题化图标适配** - 适配pixel主题化图标展示,提升视觉体验
|
||||
|
||||
#### 🎨 UI优化
|
||||
* **TimerDialog优化** - 优化TimerDialog按钮宽度设计,界面更加协调
|
||||
* **进度条交互改进** - 优化播放进度条交互体验,操作更流畅
|
||||
* **视觉一致性提升** - 改进界面视觉一致性,整体更加统一
|
||||
|
||||
#### 🐛 修复
|
||||
* **更新链接修复** - 修复更新跳转链接,现在可以正确跳转到具体版本页面
|
||||
|
||||
#### 🔧 技术改进
|
||||
* **定时功能优化** - 提升定时功能用户体验
|
||||
* **内存优化** - 进一步优化内存使用
|
||||
* **稳定性增强** - 提升播放稳定性
|
||||
|
||||
### v3.0.9 (2025-10-24)
|
||||
|
||||
#### ✨ 新功能
|
||||
* **直播开关控制** - 新增直播tab显示/隐藏开关,用户可根据需要控制直播功能
|
||||
* **实时倍速显示** - 播放控制对话框新增实时倍速数值显示,提升用户体验
|
||||
|
||||
#### 🎨 UI优化
|
||||
* **刻度显示改进** - 改进滑杆刻度显示,非激活轨道显示刻度,激活轨道保持干净
|
||||
* **播放进度条增强** - 增强播放进度条动态大小调整功能,修复圆球跳回问题
|
||||
|
||||
|
||||
### v3.0.8 (2025-10-14)
|
||||
|
||||
#### 🎨 UI交互体验全面优化
|
||||
@@ -143,11 +175,6 @@ XMBOX/
|
||||
* **修复文字重叠** - 解决跨类和换源按钮的文字重叠问题
|
||||
* **提升视觉一致性** - 整体UI视觉一致性和用户体验优化
|
||||
|
||||
#### 🔧 技术改进
|
||||
* **优化内存使用** - 改进内存管理机制
|
||||
* **提升播放稳定性** - 增强播放器稳定性
|
||||
* **文件结构重组** - 按版本号重新组织发布文件结构
|
||||
|
||||
### v3.0.5 (2025-08-20)
|
||||
#### 🎨 界面优化
|
||||
- 优化导航栏历史记录图标,采用 Material Design 3 规范的列表图标
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
# XMBOX Release Files
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
apk/release/
|
||||
├── mobile.json # 最新版本信息 (手机版)
|
||||
├── leanback.json # 最新版本信息 (TV版)
|
||||
├── v3.0.7/ # v3.0.7版本文件
|
||||
│ ├── mobile.json # v3.0.7版本信息
|
||||
│ ├── leanback.json # v3.0.7版本信息
|
||||
│ ├── mobile-arm64_v8a.apk
|
||||
│ ├── mobile-armeabi_v7a.apk
|
||||
│ ├── leanback-arm64_v8a.apk
|
||||
│ └── leanback-armeabi_v7a.apk
|
||||
└── v3.0.8/ # v3.0.8版本文件
|
||||
├── mobile.json # v3.0.8版本信息
|
||||
├── leanback.json # v3.0.8版本信息
|
||||
├── mobile-arm64_v8a-v3.0.8.apk
|
||||
├── mobile-armeabi_v7a-v3.0.8.apk
|
||||
├── leanback-arm64_v8a-v3.0.8.apk
|
||||
└── leanback-armeabi_v7a-v3.0.8.apk
|
||||
```
|
||||
|
||||
## 📱 版本说明
|
||||
|
||||
### v3.0.8 (最新版本)
|
||||
- **发布时间**: 2025-10-14
|
||||
- **版本代码**: 308
|
||||
- **主要更新**: UI交互体验全面优化
|
||||
|
||||
### v3.0.7
|
||||
- **发布时间**: 2025-09-26
|
||||
- **版本代码**: 307
|
||||
- **主要更新**: 全面优化稳定性和用户体验
|
||||
|
||||
## 🔗 下载链接
|
||||
|
||||
### 最新版本 (v3.0.8)
|
||||
- **手机版 ARM64**: [mobile-arm64_v8a-v3.0.8.apk](v3.0.8/mobile-arm64_v8a-v3.0.8.apk)
|
||||
- **手机版 ARMv7**: [mobile-armeabi_v7a-v3.0.8.apk](v3.0.8/mobile-armeabi_v7a-v3.0.8.apk)
|
||||
- **TV版 ARM64**: [leanback-arm64_v8a-v3.0.8.apk](v3.0.8/leanback-arm64_v8a-v3.0.8.apk)
|
||||
- **TV版 ARMv7**: [leanback-armeabi_v7a-v3.0.8.apk](v3.0.8/leanback-armeabi_v7a-v3.0.8.apk)
|
||||
|
||||
### 历史版本
|
||||
- **v3.0.7**: [查看v3.0.7版本文件](v3.0.7/)
|
||||
|
||||
## 📋 版本信息
|
||||
|
||||
每个版本目录都包含对应的JSON配置文件,包含:
|
||||
- `name`: 版本号
|
||||
- `desc`: 版本描述和更新内容
|
||||
- `code`: 版本代码
|
||||
- `downloads`: 下载链接映射 (仅根目录文件)
|
||||
|
||||
## 🔐 签名信息
|
||||
|
||||
所有APK文件均使用多重签名保护:
|
||||
- ✅ v1 (JAR签名) - 最佳兼容性
|
||||
- ✅ v2 (APK签名方案v2) - 全文件签名
|
||||
- ✅ v3 (APK签名方案v3) - 支持密钥轮换
|
||||
- ✅ v4 (APK签名方案v4) - 增量签名
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "3.0.8",
|
||||
"desc": "XMBOX TV版 v3.0.8 (Android TV/机顶盒专用)\n\n✨ UI优化:\n• 修复按钮点击效果过于明显的问题\n• 统一使用自定义背景替代系统selectableItemBackgroundBorderless\n• 移除Control.Action样式中的文字阴影效果\n• 优化直播页面选择按钮颜色为主题黄色\n• 调整许可协议页面按钮区域上间距为8dp\n• 修复跨类和换源按钮的文字重叠问题\n• 提升整体UI视觉一致性和用户体验\n\n🔧 改进优化:\n• 优化大屏界面体验\n• 提升播放稳定性\n\n📺 专为电视优化:遥控器导航 | 10-foot UI | ARM64/ARMv7",
|
||||
"code": 308,
|
||||
"downloads": {
|
||||
"arm64_v8a": "v3.0.8/leanback-arm64_v8a-v3.0.8.apk",
|
||||
"armeabi_v7a": "v3.0.8/leanback-armeabi_v7a-v3.0.8.apk"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "3.0.8",
|
||||
"desc": "XMBOX 手机版 v3.0.8\n\n✨ UI优化:\n• 修复按钮点击效果过于明显的问题\n• 统一使用自定义背景替代系统selectableItemBackgroundBorderless\n• 移除Control.Action样式中的文字阴影效果\n• 优化直播页面选择按钮颜色为主题黄色\n• 调整许可协议页面按钮区域上间距为8dp\n• 修复跨类和换源按钮的文字重叠问题\n• 提升整体UI视觉一致性和用户体验\n\n🔧 改进优化:\n• 优化内存使用\n• 提升播放稳定性\n\n📱 支持架构:ARM64-v8a | ARMv7a",
|
||||
"code": 308,
|
||||
"downloads": {
|
||||
"arm64_v8a": "v3.0.8/mobile-arm64_v8a-v3.0.8.apk",
|
||||
"armeabi_v7a": "v3.0.8/mobile-armeabi_v7a-v3.0.8.apk"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "3.0.7",
|
||||
"desc": "XMBOX TV版 v3.0.7 (Android TV/机顶盒专用)\n\n✨ UI优化:\n• 全新自定义开关按钮(黄色/黑色Material Design风格)\n• 优化电量百分比显示(16sp字号,2dp间距)\n• 精简设置页面,隐藏壁纸功能\n\n🔒 安全增强:\n• 启用v1/v2/v3/v4多重签名保护\n• 提升应用安全性和兼容性\n\n🔧 改进优化:\n• 修复设置页面崩溃问题\n• 优化大屏界面体验\n• 提升播放稳定性\n\n📺 专为电视优化:遥控器导航 | 10-foot UI | ARM64/ARMv7",
|
||||
"code": 307
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "3.0.7",
|
||||
"desc": "XMBOX 手机版 v3.0.7\n\n✨ UI优化:\n• 全新自定义开关按钮(黄色/黑色Material Design风格)\n• 优化电量百分比显示(16sp字号,2dp间距)\n• 精简设置页面,隐藏壁纸功能\n\n🔒 安全增强:\n• 启用v1/v2/v3/v4多重签名保护\n• 提升应用安全性和兼容性\n\n🔧 改进优化:\n• 修复设置页面崩溃问题\n• 优化内存使用\n• 提升播放稳定性\n\n📱 支持架构:ARM64-v8a | ARMv7a",
|
||||
"code": 307
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "3.0.9",
|
||||
"desc": "XMBOX TV版 v3.0.9 (Android TV/机顶盒专用)\n\n✨ 新功能:\n• 新增直播开关控制功能,可隐藏/显示直播tab\n• 新增实时倍速显示功能,播放控制对话框显示当前倍速\n• 优化源管理模块间距动态调整\n\n🎨 UI优化:\n• 滑杆圆球大小优化至20dp直径,提升操作体验\n• 改进滑杆刻度显示,非激活轨道显示刻度\n• 增强播放进度条动态大小调整功能\n• 修复播放进度条圆球跳回问题\n• 完善直播开关逻辑和UI交互\n\n🔧 改进优化:\n• 优化大屏界面体验\n• 提升播放稳定性\n• 增强UI交互体验\n\n📺 专为电视优化:遥控器导航 | 10-foot UI | ARM64/ARMv7",
|
||||
"code": 309,
|
||||
"name": "3.1.0",
|
||||
"desc": "XMBOX TV版 v3.1.0\n\n✨ 新功能:\n• 实现定时按钮倒计时显示功能\n• 适配pixel主题化图标展示\n\n🎨 UI优化:\n• 优化TimerDialog按钮宽度设计\n• 优化播放进度条交互体验\n• 改进界面视觉一致性\n\n🐛 修复:\n• 修复更新跳转链接,跳转到具体版本页面\n\n🔧 改进优化:\n• 提升定时功能用户体验\n• 优化内存使用\n• 提升播放稳定性\n\n📺 支持架构:ARM64-v8a | ARMv7a",
|
||||
"code": 310,
|
||||
"downloads": {
|
||||
"arm64_v8a": "v3.0.9/leanback-arm64_v8a-v3.0.9.apk",
|
||||
"armeabi_v7a": "v3.0.9/leanback-armeabi_v7a-v3.0.9.apk"
|
||||
"arm64_v8a": "v3.1.0/leanback-arm64_v8a-v3.1.0.apk",
|
||||
"armeabi_v7a": "v3.1.0/leanback-armeabi_v7a-v3.1.0.apk"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "3.0.9",
|
||||
"desc": "XMBOX 手机版 v3.0.9\n\n✨ 新功能:\n• 新增直播开关控制功能,可隐藏/显示直播tab\n• 新增实时倍速显示功能,播放控制对话框显示当前倍速\n• 优化源管理模块间距动态调整\n\n🎨 UI优化:\n• 滑杆圆球大小优化至20dp直径,提升操作体验\n• 改进滑杆刻度显示,非激活轨道显示刻度\n• 增强播放进度条动态大小调整功能\n• 修复播放进度条圆球跳回问题\n• 完善直播开关逻辑和UI交互\n\n🔧 改进优化:\n• 优化内存使用\n• 提升播放稳定性\n• 增强UI交互体验\n\n📱 支持架构:ARM64-v8a | ARMv7a",
|
||||
"code": 309,
|
||||
"name": "3.1.0",
|
||||
"desc": "XMBOX 手机版 v3.1.0\n\n✨ 新功能:\n• 实现定时按钮倒计时显示功能\n• 适配pixel主题化图标展示\n\n🎨 UI优化:\n• 优化TimerDialog按钮宽度设计\n• 优化播放进度条交互体验\n• 改进界面视觉一致性\n\n🐛 修复:\n• 修复更新跳转链接,跳转到具体版本页面\n\n🔧 改进优化:\n• 提升定时功能用户体验\n• 优化内存使用\n• 提升播放稳定性\n\n📱 支持架构:ARM64-v8a | ARMv7a",
|
||||
"code": 310,
|
||||
"downloads": {
|
||||
"arm64_v8a": "v3.0.9/mobile-arm64_v8a-v3.0.9.apk",
|
||||
"armeabi_v7a": "v3.0.9/mobile-armeabi_v7a-v3.0.9.apk"
|
||||
"arm64_v8a": "v3.1.0/mobile-arm64_v8a-v3.1.0.apk",
|
||||
"armeabi_v7a": "v3.1.0/mobile-armeabi_v7a-v3.1.0.apk"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "3.1.0",
|
||||
"desc": "XMBOX TV版 v3.1.0\n\n✨ 新功能:\n• 实现定时按钮倒计时显示功能\n• 适配pixel主题化图标展示\n\n🎨 UI优化:\n• 优化TimerDialog按钮宽度设计\n• 优化播放进度条交互体验\n• 改进界面视觉一致性\n\n🐛 修复:\n• 修复更新跳转链接,跳转到具体版本页面\n\n🔧 改进优化:\n• 提升定时功能用户体验\n• 优化内存使用\n• 提升播放稳定性\n\n📺 支持架构:ARM64-v8a | ARMv7a",
|
||||
"code": 310,
|
||||
"downloads": {
|
||||
"arm64_v8a": "v3.1.0/leanback-arm64_v8a-v3.1.0.apk",
|
||||
"armeabi_v7a": "v3.1.0/leanback-armeabi_v7a-v3.1.0.apk"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "3.1.0",
|
||||
"desc": "XMBOX 手机版 v3.1.0\n\n✨ 新功能:\n• 实现定时按钮倒计时显示功能\n• 适配pixel主题化图标展示\n\n🎨 UI优化:\n• 优化TimerDialog按钮宽度设计\n• 优化播放进度条交互体验\n• 改进界面视觉一致性\n\n🐛 修复:\n• 修复更新跳转链接,跳转到具体版本页面\n\n🔧 改进优化:\n• 提升定时功能用户体验\n• 优化内存使用\n• 提升播放稳定性\n\n📱 支持架构:ARM64-v8a | ARMv7a",
|
||||
"code": 310,
|
||||
"downloads": {
|
||||
"arm64_v8a": "v3.1.0/mobile-arm64_v8a-v3.1.0.apk",
|
||||
"armeabi_v7a": "v3.1.0/mobile-armeabi_v7a-v3.1.0.apk"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -92,5 +92,12 @@
|
||||
-keep class com.sun.jna.** { *; }
|
||||
-keep class com.east.android.zlive.** { *; }
|
||||
|
||||
# Media3 DefaultTimeBar - 保护反射访问的字段
|
||||
-keep class androidx.media3.ui.DefaultTimeBar {
|
||||
int barHeight;
|
||||
int scrubberEnabledSize;
|
||||
int scrubberDisabledSize;
|
||||
}
|
||||
|
||||
# Zxing
|
||||
-keep class com.google.zxing.** { *; }
|
||||
@@ -20,6 +20,7 @@ import com.github.catvod.utils.Logger;
|
||||
import com.github.catvod.utils.Path;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
@@ -32,6 +33,8 @@ public class Updater implements Download.Callback {
|
||||
private AlertDialog dialog;
|
||||
private boolean dev;
|
||||
private boolean forceCheck; // 是否为手动检查
|
||||
private String latestVersion; // 存储检测到的最新版本
|
||||
private String releaseApkUrl; // 从 GitHub Release 获取的 APK 下载链接
|
||||
|
||||
private File getFile() {
|
||||
return Path.root("Download", "XMBOX-update.apk");
|
||||
@@ -42,6 +45,12 @@ public class Updater implements Download.Callback {
|
||||
}
|
||||
|
||||
private String getApk() {
|
||||
// 优先使用从 GitHub Release 获取的 APK URL
|
||||
if (releaseApkUrl != null && !releaseApkUrl.isEmpty()) {
|
||||
Logger.d("APK download URL from Release: " + releaseApkUrl);
|
||||
return releaseApkUrl;
|
||||
}
|
||||
|
||||
// 使用JSON中指定的具体下载路径
|
||||
try {
|
||||
String response = OkHttp.string(getJson());
|
||||
@@ -55,14 +64,18 @@ public class Updater implements Download.Callback {
|
||||
String baseUrl = Github.useCnMirror() ?
|
||||
"https://gitee.com/ochenoktochen/XMBOX-Release/raw/main" :
|
||||
"https://raw.githubusercontent.com/Tosencen/XMBOX-Release/main";
|
||||
return baseUrl + "/apk/" + (dev ? "dev" : "release") + "/" + downloadPath;
|
||||
String fullUrl = baseUrl + "/apk/" + (dev ? "dev" : "release") + "/" + downloadPath;
|
||||
Logger.d("APK download URL: " + fullUrl);
|
||||
return fullUrl;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.e("Failed to get download path from JSON: " + e.getMessage());
|
||||
}
|
||||
// 回退到原来的方式
|
||||
return Github.getApk(dev, BuildConfig.FLAVOR_mode + "-" + BuildConfig.FLAVOR_abi);
|
||||
String fallbackUrl = Github.getApk(dev, BuildConfig.FLAVOR_mode + "-" + BuildConfig.FLAVOR_abi);
|
||||
Logger.d("APK fallback URL: " + fallbackUrl);
|
||||
return fallbackUrl;
|
||||
}
|
||||
|
||||
public static Updater create() {
|
||||
@@ -105,13 +118,60 @@ public class Updater implements Download.Callback {
|
||||
}
|
||||
|
||||
private void doInBackground(Activity activity) {
|
||||
Logger.d("Updater: Starting update check...");
|
||||
try {
|
||||
// 优先使用 JSON 方式检测更新(兼容性更好)
|
||||
String response = OkHttp.string(getJson());
|
||||
JSONObject object = new JSONObject(response);
|
||||
String name = object.optString("name");
|
||||
String desc = object.optString("desc");
|
||||
int code = object.optInt("code");
|
||||
|
||||
Logger.d("Updater: JSON Remote version: " + name + ", code: " + code);
|
||||
Logger.d("Updater: Local version: " + BuildConfig.VERSION_NAME + ", code: " + BuildConfig.VERSION_CODE);
|
||||
|
||||
// 使用 JSON 中的版本信息
|
||||
if (need(code, name)) {
|
||||
Logger.d("Updater: Update needed (from JSON), showing dialog");
|
||||
this.latestVersion = name; // 保存最新版本号
|
||||
|
||||
// 从 JSON 获取下载链接
|
||||
JSONObject downloads = object.optJSONObject("downloads");
|
||||
if (downloads != null) {
|
||||
String abi = BuildConfig.FLAVOR_abi;
|
||||
String downloadPath = downloads.optString(abi);
|
||||
if (!downloadPath.isEmpty()) {
|
||||
String baseUrl = Github.useCnMirror() ?
|
||||
"https://gitee.com/ochenoktochen/XMBOX-Release/raw/main" :
|
||||
"https://raw.githubusercontent.com/Tosencen/XMBOX-Release/main";
|
||||
this.releaseApkUrl = baseUrl + "/apk/" + (dev ? "dev" : "release") + "/" + downloadPath;
|
||||
Logger.d("Updater: APK URL from JSON: " + this.releaseApkUrl);
|
||||
}
|
||||
}
|
||||
|
||||
App.post(() -> show(activity, name, desc));
|
||||
} else {
|
||||
Logger.d("Updater: No update needed (from JSON)");
|
||||
if (forceCheck) {
|
||||
App.post(() -> Notify.show("已是最新版本 " + name));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.e("Updater: JSON check failed, trying GitHub API: " + e.getMessage());
|
||||
// JSON 检测失败,尝试使用 GitHub Releases API
|
||||
checkViaGitHubAPI(activity);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkViaGitHubAPI(Activity activity) {
|
||||
try {
|
||||
// 直接使用GitHub Releases API检测最新版本
|
||||
String releasesUrl = "https://api.github.com/repos/Tosencen/XMBOX/releases/latest";
|
||||
Logger.d("Updater: Trying GitHub Releases API: " + releasesUrl);
|
||||
|
||||
String response = OkHttp.string(releasesUrl);
|
||||
|
||||
// 检查响应是否包含错误信息
|
||||
if (response.contains("rate limit exceeded")) {
|
||||
Logger.e("Updater: Rate limit exceeded");
|
||||
if (forceCheck) {
|
||||
App.post(() -> Notify.show("检查更新失败:API请求过于频繁,请稍后重试"));
|
||||
}
|
||||
@@ -119,8 +179,9 @@ public class Updater implements Download.Callback {
|
||||
}
|
||||
|
||||
if (response.contains("Not Found") || response.contains("404")) {
|
||||
Logger.e("Updater: Release not found");
|
||||
if (forceCheck) {
|
||||
App.post(() -> Notify.show("检查更新失败:更新服务暂时不可用"));
|
||||
App.post(() -> Notify.show("检查更新失败:无法连接到更新服务器"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -128,31 +189,33 @@ public class Updater implements Download.Callback {
|
||||
JSONObject release = new JSONObject(response);
|
||||
String tagName = release.optString("tag_name");
|
||||
String body = release.optString("body");
|
||||
|
||||
// 提取版本号(去掉v前缀)
|
||||
String version = tagName.startsWith("v") ? tagName.substring(1) : tagName;
|
||||
|
||||
Logger.d("Updater: GitHub API Remote version: " + version);
|
||||
|
||||
// 从 assets 中查找 APK
|
||||
JSONArray assets = release.optJSONArray("assets");
|
||||
if (assets != null) {
|
||||
String targetApkName = BuildConfig.FLAVOR_mode + "-" + BuildConfig.FLAVOR_abi + "-v" + version + ".apk";
|
||||
for (int i = 0; i < assets.length(); i++) {
|
||||
JSONObject asset = assets.getJSONObject(i);
|
||||
if (targetApkName.equals(asset.optString("name"))) {
|
||||
this.releaseApkUrl = asset.optString("browser_download_url");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needUpdate(version)) {
|
||||
this.latestVersion = version;
|
||||
App.post(() -> show(activity, version, body));
|
||||
} else {
|
||||
if (forceCheck) {
|
||||
} else if (forceCheck) {
|
||||
App.post(() -> Notify.show("已是最新版本 " + version));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Logger.e("Updater: GitHub API check failed: " + e.getMessage());
|
||||
if (forceCheck) {
|
||||
App.post(() -> {
|
||||
String errorMsg = "检查更新失败";
|
||||
if (e.getMessage() != null && e.getMessage().contains("rate limit")) {
|
||||
errorMsg = "检查更新失败:请求过于频繁,请稍后重试";
|
||||
} else if (e.getMessage() != null && e.getMessage().contains("Not Found")) {
|
||||
errorMsg = "检查更新失败:更新服务暂时不可用";
|
||||
} else {
|
||||
errorMsg = "检查更新失败,请稍后重试";
|
||||
}
|
||||
Notify.show(errorMsg);
|
||||
});
|
||||
App.post(() -> Notify.show("检查更新失败:无法连接到更新服务器"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,9 +268,9 @@ public class Updater implements Download.Callback {
|
||||
}
|
||||
|
||||
private void confirm(View view) {
|
||||
// 跳转到GitHub Releases页面而不是直接下载
|
||||
// 跳转到具体版本的GitHub Releases页面
|
||||
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);
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
|
||||
@@ -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>
|
||||
@@ -29,8 +29,8 @@
|
||||
android:nextFocusUp="@id/next"
|
||||
android:nextFocusDown="@id/timeBar"
|
||||
app:bar_height="2dp"
|
||||
app:scrubber_enabled_size="12dp"
|
||||
app:scrubber_disabled_size="12dp"
|
||||
app:scrubber_enabled_size="14dp"
|
||||
app:scrubber_disabled_size="14dp"
|
||||
app:played_color="#FFEB3B"
|
||||
app:scrubber_color="#FFEB3B"
|
||||
app:buffered_color="#80FFEB3B"
|
||||
|
||||
@@ -131,6 +131,8 @@ public class App extends Application {
|
||||
OkHttp.get().setDoh(Doh.objectFrom(Setting.getDoh()));
|
||||
EventBus.builder().addIndex(new EventIndex()).installDefaultEventBus();
|
||||
CaocConfig.Builder.create().backgroundMode(CaocConfig.BACKGROUND_MODE_SILENT).errorActivity(CrashActivity.class).apply();
|
||||
// Ensure default notification channel exists for foreground playback service (TV flavor too)
|
||||
Notify.createChannel();
|
||||
|
||||
// 初始化自动缓存清理
|
||||
initCacheCleaner();
|
||||
|
||||
@@ -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");
|
||||
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")) {
|
||||
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) {
|
||||
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,6 +90,7 @@ public class JarLoader {
|
||||
}
|
||||
|
||||
public synchronized void parseJar(String key, String jar) {
|
||||
try {
|
||||
if (loaders.containsKey(key)) return;
|
||||
String[] texts = jar.split(";md5;");
|
||||
String md5 = texts.length > 1 ? texts[1].trim() : "";
|
||||
@@ -99,6 +105,10 @@ public class JarLoader {
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
|
||||
public DexClassLoader dex(String jar) {
|
||||
|
||||
@@ -59,22 +59,22 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
|
||||
timeBar.addListener(this);
|
||||
refresh = this::refresh;
|
||||
|
||||
// 设置触摸事件监听器,实现动态尺寸调整
|
||||
// 添加触摸事件处理,实现按住时圆球变大的效果
|
||||
timeBar.setOnTouchListener((v, event) -> {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
if (!isPressed) {
|
||||
isPressed = true;
|
||||
// 按下时:滑杆4dp,圆球16dp
|
||||
setTimeBarSize(4, 16);
|
||||
// 按住时:轨道变高到4dp
|
||||
setTimeBarHeight(4);
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
if (isPressed) {
|
||||
isPressed = false;
|
||||
// 松开时:滑杆2dp,圆球12dp
|
||||
setTimeBarSize(2, 12);
|
||||
// 松开时:轨道恢复到2dp
|
||||
setTimeBarHeight(2);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -95,56 +95,34 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态设置进度条高度和拖拽手柄大小
|
||||
* @param barHeightDp 滑杆高度值(dp)
|
||||
* @param scrubberSizeDp 拖拽手柄大小(dp)
|
||||
* 动态调整进度条高度
|
||||
* @param barHeightDp 轨道高度(dp)
|
||||
*/
|
||||
private void setTimeBarSize(int barHeightDp, int scrubberSizeDp) {
|
||||
// 设置滑杆高度
|
||||
private void setTimeBarHeight(int barHeightDp) {
|
||||
int barHeightPx = (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
barHeightDp,
|
||||
getContext().getResources().getDisplayMetrics()
|
||||
);
|
||||
|
||||
// 设置拖拽手柄大小
|
||||
int scrubberSizePx = (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
scrubberSizeDp,
|
||||
getContext().getResources().getDisplayMetrics()
|
||||
);
|
||||
|
||||
// 通过反射设置DefaultTimeBar的内部属性
|
||||
// 尝试通过反射设置DefaultTimeBar的内部barHeight字段
|
||||
try {
|
||||
// 设置滑杆高度
|
||||
java.lang.reflect.Field barHeightField = timeBar.getClass().getDeclaredField("barHeight");
|
||||
barHeightField.setAccessible(true);
|
||||
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.requestLayout();
|
||||
} catch (Exception e) {
|
||||
// 如果反射失败,使用备用方案
|
||||
e.printStackTrace();
|
||||
// 备用方案:重新设置布局参数
|
||||
// 如果反射失败,尝试调整布局参数
|
||||
android.util.Log.w("CustomSeekView", "Failed to set bar height via reflection: " + e.getMessage());
|
||||
if (timeBar.getLayoutParams() != null) {
|
||||
timeBar.getLayoutParams().height = barHeightPx;
|
||||
timeBar.requestLayout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void start() {
|
||||
removeCallbacks(refresh);
|
||||
@@ -224,7 +202,7 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
|
||||
positionView.setText(player.stringToTime(actualPosition));
|
||||
}
|
||||
}
|
||||
}, 50); // 延迟50ms刷新
|
||||
}, 100); // 增加延迟时间,确保拖拽状态完全结束
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -247,6 +225,7 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
|
||||
@Override
|
||||
public void onScrubStop(@NonNull TimeBar timeBar, long position, boolean canceled) {
|
||||
scrubbing = false;
|
||||
|
||||
if (!canceled) {
|
||||
// 立即设置进度条位置到目标位置,避免圆球跳回原始位置
|
||||
timeBar.setPosition(position);
|
||||
@@ -259,5 +238,7 @@ public class CustomSeekView extends FrameLayout implements TimeBar.OnScrubListen
|
||||
player.play();
|
||||
}
|
||||
}
|
||||
|
||||
// 不干预DefaultTimeBar的圆球绘制,让它自己处理
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.Manifest;
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
@@ -40,7 +41,7 @@ public class Notify {
|
||||
|
||||
public static void createChannel() {
|
||||
NotificationManagerCompat notifyMgr = NotificationManagerCompat.from(App.get());
|
||||
notifyMgr.createNotificationChannel(new NotificationChannelCompat.Builder(DEFAULT, NotificationManagerCompat.IMPORTANCE_LOW).setName("TV").build());
|
||||
notifyMgr.createNotificationChannel(new NotificationChannelCompat.Builder(DEFAULT, NotificationManagerCompat.IMPORTANCE_LOW).setName("XMBOX").build());
|
||||
}
|
||||
|
||||
public static String getError(int resId, Throwable e) {
|
||||
@@ -49,7 +50,7 @@ public class Notify {
|
||||
}
|
||||
|
||||
public static void show(Notification notification) {
|
||||
if (ActivityCompat.checkSelfPermission(App.get(), Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) return;
|
||||
if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(App.get(), Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) return;
|
||||
NotificationManagerCompat.from(App.get()).notify(ID, notification);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
<!-- 主题图标:也需要使用 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 -->
|
||||
|
||||
@@ -28,4 +28,7 @@
|
||||
<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>
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.github.catvod.utils.Logger;
|
||||
import com.github.catvod.utils.Path;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
@@ -32,6 +33,8 @@ public class Updater implements Download.Callback {
|
||||
private AlertDialog dialog;
|
||||
private boolean dev;
|
||||
private boolean forceCheck; // 是否为手动检查
|
||||
private String latestVersion; // 存储检测到的最新版本
|
||||
private String releaseApkUrl; // 从 GitHub Release 获取的 APK 下载链接
|
||||
|
||||
private File getFile() {
|
||||
return Path.root("Download", "XMBOX-update.apk");
|
||||
@@ -42,6 +45,12 @@ public class Updater implements Download.Callback {
|
||||
}
|
||||
|
||||
private String getApk() {
|
||||
// 优先使用从 GitHub Release 获取的 APK URL
|
||||
if (releaseApkUrl != null && !releaseApkUrl.isEmpty()) {
|
||||
Logger.d("APK download URL from Release: " + releaseApkUrl);
|
||||
return releaseApkUrl;
|
||||
}
|
||||
|
||||
// 使用JSON中指定的具体下载路径
|
||||
try {
|
||||
String response = OkHttp.string(getJson());
|
||||
@@ -111,14 +120,56 @@ public class Updater implements Download.Callback {
|
||||
private void doInBackground(Activity activity) {
|
||||
Logger.d("Updater: Starting update check...");
|
||||
try {
|
||||
// 直接使用GitHub Releases API检测最新版本
|
||||
// 优先使用 JSON 方式检测更新(兼容性更好)
|
||||
String response = OkHttp.string(getJson());
|
||||
JSONObject object = new JSONObject(response);
|
||||
String name = object.optString("name");
|
||||
String desc = object.optString("desc");
|
||||
int code = object.optInt("code");
|
||||
|
||||
Logger.d("Updater: JSON Remote version: " + name + ", code: " + code);
|
||||
Logger.d("Updater: Local version: " + BuildConfig.VERSION_NAME + ", code: " + BuildConfig.VERSION_CODE);
|
||||
|
||||
// 使用 JSON 中的版本信息
|
||||
if (need(code, name)) {
|
||||
Logger.d("Updater: Update needed (from JSON), showing dialog");
|
||||
this.latestVersion = name; // 保存最新版本号
|
||||
|
||||
// 从 JSON 获取下载链接
|
||||
JSONObject downloads = object.optJSONObject("downloads");
|
||||
if (downloads != null) {
|
||||
String abi = BuildConfig.FLAVOR_abi;
|
||||
String downloadPath = downloads.optString(abi);
|
||||
if (!downloadPath.isEmpty()) {
|
||||
String baseUrl = Github.useCnMirror() ?
|
||||
"https://gitee.com/ochenoktochen/XMBOX-Release/raw/main" :
|
||||
"https://raw.githubusercontent.com/Tosencen/XMBOX-Release/main";
|
||||
this.releaseApkUrl = baseUrl + "/apk/" + (dev ? "dev" : "release") + "/" + downloadPath;
|
||||
Logger.d("Updater: APK URL from JSON: " + this.releaseApkUrl);
|
||||
}
|
||||
}
|
||||
|
||||
App.post(() -> show(activity, name, desc));
|
||||
} else {
|
||||
Logger.d("Updater: No update needed (from JSON)");
|
||||
if (forceCheck) {
|
||||
App.post(() -> Notify.show("已是最新版本 " + name));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.e("Updater: JSON check failed, trying GitHub API: " + e.getMessage());
|
||||
// JSON 检测失败,尝试使用 GitHub Releases API
|
||||
checkViaGitHubAPI(activity);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkViaGitHubAPI(Activity activity) {
|
||||
try {
|
||||
String releasesUrl = "https://api.github.com/repos/Tosencen/XMBOX/releases/latest";
|
||||
Logger.d("Updater: GitHub Releases API URL: " + releasesUrl);
|
||||
Logger.d("Updater: Trying GitHub Releases API: " + releasesUrl);
|
||||
|
||||
String response = OkHttp.string(releasesUrl);
|
||||
Logger.d("Updater: API response length: " + response.length());
|
||||
|
||||
// 检查响应是否包含错误信息
|
||||
if (response.contains("rate limit exceeded")) {
|
||||
Logger.e("Updater: Rate limit exceeded");
|
||||
if (forceCheck) {
|
||||
@@ -130,7 +181,7 @@ public class Updater implements Download.Callback {
|
||||
if (response.contains("Not Found") || response.contains("404")) {
|
||||
Logger.e("Updater: Release not found");
|
||||
if (forceCheck) {
|
||||
App.post(() -> Notify.show("检查更新失败:更新服务暂时不可用"));
|
||||
App.post(() -> Notify.show("检查更新失败:无法连接到更新服务器"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -138,28 +189,33 @@ public class Updater implements Download.Callback {
|
||||
JSONObject release = new JSONObject(response);
|
||||
String tagName = release.optString("tag_name");
|
||||
String body = release.optString("body");
|
||||
|
||||
// 提取版本号(去掉v前缀)
|
||||
String version = tagName.startsWith("v") ? tagName.substring(1) : tagName;
|
||||
|
||||
Logger.d("Updater: Remote version: " + version);
|
||||
Logger.d("Updater: Local version: " + BuildConfig.VERSION_NAME);
|
||||
Logger.d("Updater: GitHub API Remote version: " + version);
|
||||
|
||||
// 从 assets 中查找 APK
|
||||
JSONArray assets = release.optJSONArray("assets");
|
||||
if (assets != null) {
|
||||
String targetApkName = BuildConfig.FLAVOR_mode + "-" + BuildConfig.FLAVOR_abi + "-v" + version + ".apk";
|
||||
for (int i = 0; i < assets.length(); i++) {
|
||||
JSONObject asset = assets.getJSONObject(i);
|
||||
if (targetApkName.equals(asset.optString("name"))) {
|
||||
this.releaseApkUrl = asset.optString("browser_download_url");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 比较版本号
|
||||
if (needUpdate(version)) {
|
||||
Logger.d("Updater: Update needed, showing dialog");
|
||||
this.latestVersion = version;
|
||||
App.post(() -> show(activity, version, body));
|
||||
} else {
|
||||
Logger.d("Updater: No update needed");
|
||||
if (forceCheck) {
|
||||
} else if (forceCheck) {
|
||||
App.post(() -> Notify.show("已是最新版本 " + version));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.e("Updater: Exception during update check: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
Logger.e("Updater: GitHub API check failed: " + e.getMessage());
|
||||
if (forceCheck) {
|
||||
App.post(() -> Notify.show("检查更新失败:网络连接异常"));
|
||||
App.post(() -> Notify.show("检查更新失败:无法连接到更新服务器"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,9 +267,9 @@ public class Updater implements Download.Callback {
|
||||
}
|
||||
|
||||
private void confirm(View view) {
|
||||
// 跳转到GitHub Releases页面而不是直接下载
|
||||
// 跳转到具体版本的GitHub Releases页面
|
||||
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);
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="本项目仅用于学习Android开发,代码改自FongMi/TV (https://github.com/FongMi/TV)。"
|
||||
android:text="本项目仅用于学习Android开发,代码改自FongMi/TV项目。"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_weight="1"
|
||||
app:bar_height="2dp"
|
||||
app:scrubber_enabled_size="12dp"
|
||||
app:scrubber_disabled_size="12dp"
|
||||
app:scrubber_enabled_size="14dp"
|
||||
app:scrubber_disabled_size="14dp"
|
||||
app:played_color="#FFEB3B"
|
||||
app:scrubber_color="#FFEB3B"
|
||||
app:buffered_color="#80FFEB3B" />
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd /Users/chen/Desktop/XMBOX
|
||||
./gradlew clean assembleMobileArm64_v8aDebug
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. 🛡️ **片头片尾跳过** - 处理嵌入式广告
|
||||
|
||||
让您享受**纯净、流畅、安全**的视频观看体验!
|
||||
|
||||
@@ -491,7 +491,7 @@ include ':thunder'
|
||||
include ':tvbus'
|
||||
include ':zlive'
|
||||
|
||||
rootProject.name = "TV"
|
||||
rootProject.name = "XMBOX"
|
||||
EOF
|
||||
|
||||
echo -e "${GREEN}依赖修复完成!${NC}"
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# 配置Java路径,使用Java 17而不是默认的Java 21
|
||||
org.gradle.java.home=/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home
|
||||
|
||||
# 增加构建内存
|
||||
org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=1024m -XX:+HeapDumpOnOutOfMemoryError
|
||||
|
||||
# 启用并行构建
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
|
||||
# 配置网络设置
|
||||
systemProp.https.protocols=TLSv1.2,TLSv1.3
|
||||
systemProp.https.proxyPort=0
|
||||
systemProp.https.nonProxyHosts=localhost
|
||||
|
||||
# Android相关配置
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.jetifier.ignorelist=bcprov-jdk15on,annotation-experimental-1.4.1.aar,activity-1.8.0.aar,nextlib-media3ext-0.8.4.aar,sardine-android-0.9.aar,bcprov-jdk18on-1.79.jar
|
||||
|
||||
# 允许高版本的SDK
|
||||
android.suppressUnsupportedCompileSdk=35
|
||||
|
||||
# 禁用增量编译以解决某些兼容性问题
|
||||
android.enableBuildIncremental=false
|
||||
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -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解析时生效",
|
||||
"隐私保护": "所有拦截在本地进行,不上传任何数据"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
git clone --mirror https://github.com/FongMi/Release.git
|
||||
java -jar bfg.jar --delete-files *.apk Release.git
|
||||
java -jar bfg.jar --delete-files *.json Release.git
|
||||
cd Release.git
|
||||
git reflog expire --expire=now --all && git gc --prune=now --aggressive
|
||||
git push
|
||||
git gc
|
||||
@@ -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 "=== 完成! ==="
|
||||
|
||||
@@ -20,6 +20,6 @@ dependencyResolutionManagement {
|
||||
}
|
||||
include ':app'
|
||||
include ':catvod'
|
||||
include ':chaquo'
|
||||
// include ':chaquo' // 已移除Python支持
|
||||
include ':quickjs'
|
||||
rootProject.name = "TV"
|
||||
rootProject.name = "XMBOX"
|
||||
|
||||