Compare commits

..
18 Commits
Author SHA1 Message Date
Li ZongYing d03de91c1e request optimize 2024-03-16 10:44:55 +08:00
Li ZongYing 7057e4a8b4 request optimize 2024-03-15 22:32:43 +08:00
Li ZongYing 188431a29f fix 4.0 crash 2024-03-14 22:08:28 +08:00
Li ZongYing 5c0152e36c optimize fetch token 2024-03-13 12:11:51 +08:00
Li ZongYing a63463559f phoenix retry 2024-03-13 11:31:37 +08:00
Li ZongYing d37ada1268 phoenix epg 2024-03-12 16:16:07 +08:00
Li ZongYing 00e6c8d66e cetv1 2024-03-07 19:37:03 +08:00
Li ZongYing 0ebc166acc bootStartup->false 2024-03-06 01:20:44 +08:00
Li ZongYing 2975a61e14 test RecyclerView alpha 2024-02-22 21:26:55 +08:00
Li ZongYing 86b9f5159a readme 2024-02-22 11:16:12 +08:00
Li ZongYing 10f261be8b fix save state 2024-02-21 21:57:05 +08:00
Li ZongYing 52d356817a fix some keys invalid 2024-02-21 19:26:11 +08:00
Li ZongYing 6ede7f76ef auto auth 2024-02-21 16:36:37 +08:00
Li ZongYing a4da01adf4 new memu style 2024-02-21 15:14:06 +08:00
Li ZongYing 8b33c7c94c optimize memu 2024-02-20 20:55:35 +08:00
Li ZongYing 84a6c0f78e menu delay disappears 2024-02-20 11:33:30 +08:00
Li ZongYing 1417bbf57b auto-startup 2024-02-19 15:57:39 +08:00
Li ZongYing ce678b8741 test release api 2024-02-11 10:14:23 +08:00
48 changed files with 2490 additions and 1648 deletions
+83
View File
@@ -0,0 +1,83 @@
name: Bug 反馈
description: 反馈一个 Bug
labels: [ "bug" ]
title: "[BUG] "
body:
- type: checkboxes
id: checklist
attributes:
label: 检查清单
description: 确保我们的错误报告表单适合您。
options:
- label: 之前没有人提交过类似或相同的 bug report。
required: true
- label: 我正在使用本软件的最新版本。
required: true
- type: dropdown
id: version
attributes:
label: my-ty 版本
description: 请选择正在使用的版本
options:
- 通用版
- 专用版
validations:
required: true
- type: textarea
id: bug
attributes:
label: Bug 描述
description: 请描述 bug 详情
placeholder: |
e.g. Crashed when generating snapshot.
validations:
required: true
- type: textarea
id: expected
attributes:
label: 预期行为
description: 你预期会发生什么?
placeholder: |
e.g. A New snapshot!
validations:
required: true
- type: textarea
id: actual
attributes:
label: 实际行为
description: 反而发生了什么?
placeholder: |
e.g. Crashed.
validations:
required: true
- type: textarea
id: steps
attributes:
label: 复现步骤
description: 如何复现这个 bug。
placeholder: |
1. Open the app
2. Crashed
What an app.
- type: input
id: ui
attributes:
label: UI / OS
description: 你的电视系统 UI 或 OS 或 品牌
placeholder: TCL / XIAOMI / PHONE / etc.
validations:
required: true
- type: input
id: android
attributes:
label: Android 版本
description: 你的 Android 版本
placeholder: "12"
validations:
required: true
- type: textarea
id: additional
attributes:
label: 额外信息
description: 任何你觉得值得说的。
+36
View File
@@ -0,0 +1,36 @@
name: 功能(新频道)请求
description: 提出一个建议
labels: [ "enhancement" ]
title: "[FR] "
body:
- type: checkboxes
id: checklist
attributes:
label: 检查清单
description: 确保我们的错误报告表单适合您。
options:
- label: 之前没有人提交过类似或相同的功能请求。
required: true
- label: 这个建议不会背离 my-tv 的初衷。
required: true
- type: textarea
id: propose
attributes:
label: 改进目的
description: 改进有什么用
placeholder: |
Show your idea here.
validations:
required: true
- type: textarea
id: solution
attributes:
label: 解决方案
description: 你会怎么完成这个改进?
placeholder: |
How to do it on your opinion? Or left this blank
- type: textarea
id: addition
attributes:
label: 额外信息
description: 任何你觉得值得说的。
+133 -17
View File
@@ -6,7 +6,7 @@
下载安装 [releases](https://github.com/lizongying/my-tv/releases/) 下载安装 [releases](https://github.com/lizongying/my-tv/releases/)
其他地址 [my-tv](https://lyrics.run/my-tv.html) 更多地址 [my-tv](https://lyrics.run/my-tv.html)
![image](./screenshots/img_3.png) ![image](./screenshots/img_3.png)
![image](./screenshots/img_2.png) ![image](./screenshots/img_2.png)
@@ -14,56 +14,144 @@
## 更新日志 ## 更新日志
### v1.5.8(通用版) ### v1.6.6(通用)
* 更新重庆卫视图标
* 凤凰卫视增强画质
* 凤凰卫视增加EPG
### v1.6.5(安卓5及以上专用)
* 增加CETV1图标
* 稳定性提升
### v1.6.4(通用)
* 增加CETV1
* 增加凤凰卫视
* 默认关闭开机启动
### v1.6.3(安卓5及以上专用)
* 增加CETV1
* 凤凰卫视增强画质
* 默认关闭开机启动
* 延迟菜单自动关闭时间
* 解决一些可能导致首次打开时黑屏的问题
### v1.6.2(通用)
* 修复按键无效的问题
* 新的频道列表样式
### v1.6.1(安卓5及以上专用)
* 增加凤凰卫视
### v1.6.0(通用)
* 通用(春晚緊急修復)
### v1.5.9(安卓5及以上专用)
* 解决天猫魔盒闪退问题
### v1.5.8(通用)
* 修复央视6画质差的问题 * 修复央视6画质差的问题
* 增加兵团卫视 * 增加兵团卫视
* 播放失败重试 * 播放失败重试
### v1.5.6(通用版) ### v1.5.7(安卓5及以上专用)
* 修复播放失败的问题
### v1.5.6(通用)
* 解决部分设备系统时间不对导致播放失败的问题 * 解决部分设备系统时间不对导致播放失败的问题
### v1.5.2(通用版) ### v1.5.5(安卓5及以上专用)
* 修复播放失败的问题
* 修复APP恢复后频道号、频道列表不自动消失的问题
### v1.5.4(通用)
* 修复播放失败的问题
### 以下已不可用
### v1.5.3(安卓5及以上专用)
* 修复部分情况下APP切换后无法继续播放的问题
* 优化重试逻辑
### v1.5.2(通用)
* 修复APP恢复后频道号、频道列表不自动消失的问题 * 修复APP恢复后频道号、频道列表不自动消失的问题
### v1.5.1(高版本专用) ### v1.5.1(安卓5及以上专用)
* 性能优化 * 性能优化
### v1.5.0(通用版) ### v1.5.0(通用)
* 修复部分情况下APP切换后无法继续播放的问题 * 修复部分情况下APP切换后无法继续播放的问题
### v1.4.9(高版本专用) ### v1.4.9(安卓5及以上专用)
* 同步v1.4.8 * 同步v1.4.8
### v1.4.8(通用版) ### v1.4.8(通用)
* 频道号从1开始,CCTV5+为18 * 频道号从1开始,CCTV5+为18
* 提高CCTV6清晰度 * 提高CCTV6清晰度
* 增加天津卫视、新疆卫视 * 增加天津卫视、新疆卫视
### v1.4.7(高版本专用) ### v1.4.7(安卓5及以上专用)
* 修复部分用户CCTV13播放过程中卡住的问题 * 修复部分用户CCTV13播放过程中卡住的问题
* 调整CCTV的频道顺序 * 调整CCTV的频道顺序
### v1.4.6(通用版) ### v1.4.6(通用)
* 10以下频道不再需要先按0 * 10以下频道不再需要先按0
### v1.4.5(高版本专用) ### v1.4.5(安卓5及以上专用)
* 数字选台配置 * 数字选台配置
### v1.4.4(通用版) ### v1.4.4(通用)
* 优化图标显示 * 优化图标显示
* 增加换台反转 * 增加换台反转
### v1.4.3(安卓5及以上专用)
* 支持频道反转配置
### v1.4.2(通用)
* 支持安卓4
### v1.4.1
* 解决部分用户无法打开菜单的问题
### v1.4.0
* 解决极个别高版本机型黑屏问题
### v1.3.9
* 提高稳定性
* 提高连接速度
### v1.3.4
* 部分错误会提示用户
* 菜单3秒钟后自动关闭
### v1.3.3 ### v1.3.3
* 部分错误会提示用户 * 部分错误会提示用户
@@ -74,6 +162,27 @@
* 增加重试,减少因网络问题导致的播放失败 * 增加重试,减少因网络问题导致的播放失败
* 优化横幅banner * 优化横幅banner
### v1.3.1
* 增加CCTV 8K 超高清频道
* 增加国际频道栏目
### v1.3.0
* 处理368可能失败的情形
### v1.2.9
* 支持3、6、8
### v1.2.8
* 兼容16:10等分辨率
### v1.2.7
* 此版本是为了测试安卓6.0以下版本,如之前可以正常运行,请勿安装。
### v1.2.6 ### v1.2.6
* 支持安卓4.2 * 支持安卓4.2
@@ -94,6 +203,14 @@
* 播放相同的频道,不再重复加载 * 播放相同的频道,不再重复加载
* 暂时移除部分频道 * 暂时移除部分频道
### v1.2.3
* 固定视频比列为16:9
* 隐藏全面屏底部的小横条
* 移除移动专区
* 修复一个闪退问题
* 更换图标和应用名
## 其他 ## 其他
小米电视可以使用小米电视助手进行安装 小米电视可以使用小米电视助手进行安装
@@ -107,19 +224,18 @@ adb install my-tv.apk
## TODO ## TODO
* 音量不同 * 音量不同
* 大湾区卫视、广东4k超高清、广东珠江 * 大湾区卫视、广东4k超高清、广东珠江、三沙卫视
* CETV教育频道 * CETV教育频道
* CHC高清三个电影频道 * CHC高清三个电影频道
* 地方频道 * 地方频道
* 收藏夹 * 收藏夹
* 自定义源 * 自定义源
* 凤凰卫视、凤凰资讯台
* 海外 * 海外
* 1.5.0 无法安装,1.5.1 可以安装 * 1.5.0 无法安装,1.5.1 可以安装
* 获取系统时间
* 选中的图标比例能否相差更大
* 自动重连
* 节目增加预告 * 节目增加预告
* 频道列表优化
* epg更新后菜单更新
* 防止菜单第一个获取焦点
## 赞赏 ## 赞赏
+4
View File
@@ -113,4 +113,8 @@ dependencies {
implementation 'com.github.bumptech.glide:glide:4.11.0' implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0-RC") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0-RC")
implementation 'com.google.android.exoplayer:exoplayer-ui:2.13.3'
implementation 'com.google.android.exoplayer:exoplayer-core:2.13.3'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.13.3'
} }
+13
View File
@@ -7,6 +7,7 @@
android:name="android.software.leanback" android:name="android.software.leanback"
android:required="true" /> android:required="true" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:largeHeap="true" android:largeHeap="true"
@@ -29,5 +30,17 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<receiver
android:name=".BootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<provider
android:name=".InitializerProvider"
android:authorities="${applicationId}.InitializerProvider"
android:exported="false"/>
</application> </application>
</manifest> </manifest>
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,18 @@
package com.lizongying.mytv
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (SP.bootStartup) {
context.startActivity(
Intent(context, MainActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
}
}
}
@@ -2,6 +2,7 @@ package com.lizongying.mytv
import android.graphics.Color import android.graphics.Color
import android.view.ContextThemeWrapper import android.view.ContextThemeWrapper
import android.view.KeyEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.ScaleAnimation import android.view.animation.ScaleAnimation
@@ -14,31 +15,40 @@ import com.lizongying.mytv.models.TVListViewModel
import com.lizongying.mytv.models.TVViewModel import com.lizongying.mytv.models.TVViewModel
class CardAdapter(private val owner: LifecycleOwner, private var tvListViewModel: TVListViewModel) : class CardAdapter(
private val recyclerView: RecyclerView,
private val mainFragment: MainFragment,
private var tvListViewModel: TVListViewModel,
) :
RecyclerView.Adapter<CardAdapter.ViewHolder>() { RecyclerView.Adapter<CardAdapter.ViewHolder>() {
private var listener: ItemListener? = null private var listener: ItemListener? = null
private var focused: View? = null
var visiable = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val cardView = object : val cardView = object :
ImageCardView(ContextThemeWrapper(parent.context, R.style.CustomImageCardTheme)) {} ImageCardView(ContextThemeWrapper(parent.context, R.style.CustomImageCardTheme)) {}
startScaleAnimation(cardView, 1.0f, 0.9f)
cardView.isFocusable = true cardView.isFocusable = true
cardView.isFocusableInTouchMode = true cardView.isFocusableInTouchMode = true
return ViewHolder(cardView) return ViewHolder(cardView)
} }
private fun startScaleAnimation(view: View, fromScale: Float, toScale: Float) { fun clear() {
focused?.clearFocus()
recyclerView.invalidate()
}
private fun startScaleAnimation(view: View, fromScale: Float, toScale: Float, duration: Long) {
val scaleAnimation = ScaleAnimation( val scaleAnimation = ScaleAnimation(
fromScale, toScale, fromScale, toScale,
fromScale, toScale, fromScale, toScale,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f, ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f ScaleAnimation.RELATIVE_TO_SELF, 0.5f
) )
scaleAnimation.duration = 200 scaleAnimation.duration = duration
scaleAnimation.fillAfter = true scaleAnimation.fillAfter = false
view.startAnimation(scaleAnimation) view.startAnimation(scaleAnimation)
} }
@@ -47,58 +57,54 @@ class CardAdapter(private val owner: LifecycleOwner, private var tvListViewModel
val tvViewModel = item as TVViewModel val tvViewModel = item as TVViewModel
val cardView = viewHolder.view as ImageCardView val cardView = viewHolder.view as ImageCardView
cardView.tag = tvViewModel
cardView.setOnFocusChangeListener { _, hasFocus -> val onFocusChangeListener = View.OnFocusChangeListener { view, hasFocus ->
listener?.onItemFocusChange(item, hasFocus)
if (hasFocus) { if (hasFocus) {
startScaleAnimation(cardView, 0.9f, 1.0f) listener?.onItemHasFocus(item)
} else { focused = cardView
startScaleAnimation(cardView, 1.0f, 0.9f)
startScaleAnimation(cardView, 0.9f, 1.0f, 200)
//
// if (mainFragment.shouldHasFocus(view.tag as TVViewModel)) {
// }
// if (visiable) {
// startScaleAnimation(cardView, 0.9f, 1.0f, 200)
// } else {
// visiable = true
// }
} }
} }
cardView.onFocusChangeListener = onFocusChangeListener
cardView.setOnClickListener { _ -> cardView.setOnClickListener { _ ->
listener?.onItemClicked(item) listener?.onItemClicked(item)
} }
cardView.titleText = tvViewModel.title.value cardView.setOnKeyListener { _, keyCode, event: KeyEvent? ->
cardView.tag = tvViewModel.videoUrl.value if (event?.action == KeyEvent.ACTION_DOWN) {
return@setOnKeyListener listener?.onKey(keyCode) ?: false
when (tvViewModel.title.value) { }
"CCTV8K 超高清" -> Glide.with(viewHolder.view.context) false
.load(R.drawable.cctv8k)
.centerInside()
.into(cardView.mainImageView)
"天津卫视" -> Glide.with(viewHolder.view.context)
.load(R.drawable.tianjin)
.centerInside()
.into(cardView.mainImageView)
"新疆卫视" -> Glide.with(viewHolder.view.context)
.load(R.drawable.xinjiang)
.centerInside()
.into(cardView.mainImageView)
"兵团卫视" -> Glide.with(viewHolder.view.context)
.load(R.drawable.bingtuan)
.centerInside()
.into(cardView.mainImageView)
else -> Glide.with(viewHolder.view.context)
.load(tvViewModel.logo.value)
.centerInside()
.into(cardView.mainImageView)
} }
cardView.titleText = tvViewModel.title.value
Glide.with(viewHolder.view.context)
.load(tvViewModel.logo.value)
.centerInside()
.into(cardView.mainImageView)
cardView.mainImageView.setBackgroundColor(Color.WHITE) cardView.mainImageView.setBackgroundColor(Color.WHITE)
cardView.setMainImageScaleType(ImageView.ScaleType.CENTER_INSIDE) cardView.setMainImageScaleType(ImageView.ScaleType.CENTER_INSIDE)
tvViewModel.program.observe(owner) { _ -> val epg = tvViewModel.epg.value?.filter { it.beginTime < Utils.getDateTimestamp() }
val program = tvViewModel.getProgramOne() if (!epg.isNullOrEmpty()) {
if (program != null) { cardView.contentText = epg.last().title
cardView.contentText = program.name } else {
} cardView.contentText = ""
} }
} }
@@ -108,9 +114,18 @@ class CardAdapter(private val owner: LifecycleOwner, private var tvListViewModel
val view = itemView val view = itemView
} }
fun toPosition(position: Int) {
recyclerView.post {
recyclerView.scrollToPosition(position)
recyclerView.getChildAt(position)?.isSelected
recyclerView.getChildAt(position)?.requestFocus()
}
}
interface ItemListener { interface ItemListener {
fun onItemFocusChange(tvViewModel: TVViewModel, hasFocus: Boolean) fun onItemHasFocus(tvViewModel: TVViewModel)
fun onItemClicked(tvViewModel: TVViewModel) fun onItemClicked(tvViewModel: TVViewModel)
fun onKey(keyCode: Int): Boolean
} }
fun setItemListener(listener: ItemListener) { fun setItemListener(listener: ItemListener) {
@@ -1,82 +0,0 @@
package com.lizongying.mytv
import android.graphics.Color
import android.view.ContextThemeWrapper
import android.view.ViewGroup
import android.widget.ImageView
import androidx.leanback.widget.ImageCardView
import androidx.leanback.widget.Presenter
import androidx.lifecycle.LifecycleOwner
import com.bumptech.glide.Glide
import com.lizongying.mytv.models.TVViewModel
class CardPresenter(
private val owner: LifecycleOwner,
) : Presenter() {
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
val cardView = object :
ImageCardView(ContextThemeWrapper(parent.context, R.style.CustomImageCardTheme)) {}
cardView.isFocusable = true
cardView.isFocusableInTouchMode = true
return ViewHolder(cardView)
}
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
val tvViewModel = item as TVViewModel
val cardView = viewHolder.view as ImageCardView
cardView.titleText = tvViewModel.title.value
cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT)
cardView.tag = tvViewModel.videoUrl.value
when (tvViewModel.title.value) {
"CCTV8K 超高清" -> Glide.with(viewHolder.view.context)
.load(R.drawable.cctv8k)
.centerInside()
.into(cardView.mainImageView)
"天津卫视" -> Glide.with(viewHolder.view.context)
.load(R.drawable.tianjin)
.centerInside()
.into(cardView.mainImageView)
"新疆卫视" -> Glide.with(viewHolder.view.context)
.load(R.drawable.xinjiang)
.centerInside()
.into(cardView.mainImageView)
"兵团卫视" -> Glide.with(viewHolder.view.context)
.load(R.drawable.bingtuan)
.centerInside()
.into(cardView.mainImageView)
else -> Glide.with(viewHolder.view.context)
.load(tvViewModel.logo.value)
.centerInside()
.into(cardView.mainImageView)
}
cardView.setBackgroundColor(Color.WHITE)
cardView.setMainImageScaleType(ImageView.ScaleType.CENTER_INSIDE)
tvViewModel.program.observe(owner) { _ ->
val program = tvViewModel.getProgramOne()
if (program != null) {
cardView.contentText = program.name
}
}
}
override fun onUnbindViewHolder(viewHolder: ViewHolder) {
val cardView = viewHolder.view as ImageCardView
cardView.mainImage = null
}
companion object {
private const val TAG = "CardPresenter"
private const val CARD_WIDTH = 300
private const val CARD_HEIGHT = 101
}
}
@@ -0,0 +1,84 @@
@file:Suppress("DEPRECATION")
package com.lizongying.mytv
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.Signature
import android.content.pm.SigningInfo
import android.os.Build
import android.util.Log
import java.security.MessageDigest
private const val TAG = "Extensions"
private val Context.packageInfo: PackageInfo
get() {
val flag = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
PackageManager.GET_SIGNATURES
} else {
PackageManager.GET_SIGNING_CERTIFICATES
}
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackageInfo(packageName, flag)
} else {
packageManager.getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(PackageManager.GET_SIGNING_CERTIFICATES.toLong())
)
}
}
/**
* Return the version code of the app which is defined in build.gradle.
* eg:100
*/
val Context.appVersionCode: Long
get() {
val packageInfo = this.packageInfo
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode
} else {
packageInfo.versionCode.toLong()
}
}
/**
* Return the version name of the app which is defined in build.gradle.
* eg:1.0.0
*/
val Context.appVersionName: String get() = packageInfo.versionName
val Context.appSignature: String
get() {
val packageInfo = this.packageInfo
var sign: Signature? = null
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
val signatures: Array<out Signature>? = packageInfo.signatures
if (signatures != null) {
sign = signatures[0]
}
} else {
val signingInfo: SigningInfo? = packageInfo.signingInfo
if (signingInfo != null) {
sign = signingInfo.apkContentsSigners[0]
}
}
if (sign == null) {
return ""
}
return hashSignature(sign)
}
private fun hashSignature(signature: Signature): String {
return try {
val md = MessageDigest.getInstance("MD5")
md.update(signature.toByteArray())
val digest = md.digest()
digest.let { it -> it.joinToString("") { "%02x".format(it) } }
} catch (e: Exception) {
Log.e(TAG, "Error hashing signature", e)
""
}
}
@@ -0,0 +1,51 @@
package com.lizongying.mytv
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.view.View
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
class GrayOverlayItemDecoration(private val context: Context) : RecyclerView.ItemDecoration() {
private val grayOverlayPaint = Paint().apply {
color = ContextCompat.getColor(context, R.color.gray_overlay)
style = Paint.Style.FILL
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
val childCount = parent.childCount
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
if (!child.hasFocus()) {
// child.alpha = 0.7f
// 计算遮罩层的大小
val overlayRect = Rect(
child.left,
child.top,
child.right,
child.bottom
)
// 绘制灰色遮罩层
c.drawRect(overlayRect, grayOverlayPaint)
} else {
// child.alpha = 1f
}
}
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
// 在此处设置偏移量为0,以防止遮罩层影响项的布局
outRect.setEmpty()
}
}
@@ -2,6 +2,7 @@ package com.lizongying.mytv
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -30,31 +31,16 @@ class InfoFragment : Fragment() {
fun show(tvViewModel: TVViewModel) { fun show(tvViewModel: TVViewModel) {
binding.textView.text = tvViewModel.title.value binding.textView.text = tvViewModel.title.value
when (tvViewModel.title.value) { Glide.with(this)
"CCTV8K 超高清" -> Glide.with(this) .load(tvViewModel.logo.value)
.load(R.drawable.cctv8k) .into(binding.infoLogo)
.into(binding.infoLogo)
"天津卫视" -> Glide.with(this) Log.i(TAG, "${tvViewModel.title.value} ${tvViewModel.epg.value}")
.load(R.drawable.tianjin) val epg = tvViewModel.epg.value?.filter { it.beginTime < Utils.getDateTimestamp() }
.into(binding.infoLogo) if (!epg.isNullOrEmpty()) {
binding.infoDesc.text = epg.last().title
"新疆卫视" -> Glide.with(this) } else {
.load(R.drawable.xinjiang) binding.infoDesc.text = ""
.into(binding.infoLogo)
"兵团卫视" -> Glide.with(this)
.load(R.drawable.bingtuan)
.into(binding.infoLogo)
else -> Glide.with(this)
.load(tvViewModel.logo.value)
.into(binding.infoLogo)
}
val program = tvViewModel.getProgramOne()
if (program != null) {
binding.infoDesc.text = program.name
} }
handler.removeCallbacks(removeRunnable) handler.removeCallbacks(removeRunnable)
@@ -0,0 +1,39 @@
package com.lizongying.mytv
import android.content.ContentProvider
import android.content.ContentValues
import android.net.Uri
internal class InitializerProvider : ContentProvider() {
// Happens before Application#onCreate.It's fine to init something here
override fun onCreate(): Boolean {
SP.init(context!!)
return true
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?,
) = unsupported()
override fun getType(uri: Uri) = unsupported()
override fun insert(uri: Uri, values: ContentValues?) = unsupported()
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?) =
unsupported()
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?,
) = unsupported()
private fun unsupported(errorMessage: String? = null): Nothing =
throw UnsupportedOperationException(errorMessage)
}
@@ -1,12 +1,5 @@
package com.lizongying.mytv package com.lizongying.mytv
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.Signature
import android.content.pm.SigningInfo
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
@@ -18,53 +11,34 @@ import android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
import android.view.WindowManager import android.view.WindowManager
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.lizongying.mytv.models.TVViewModel import com.lizongying.mytv.models.TVViewModel
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import java.security.MessageDigest
class MainActivity : FragmentActivity() { class MainActivity : FragmentActivity(), Request.RequestListener {
private var ready = 0 private var ready = 0
private var playerFragment = PlayerFragment() private val playerFragment = PlayerFragment()
private var mainFragment = MainFragment() private val mainFragment = MainFragment()
private var infoFragment = InfoFragment() private val infoFragment = InfoFragment()
private var channelFragment = ChannelFragment() private val channelFragment = ChannelFragment()
private lateinit var settingFragment: SettingFragment private val settingFragment = SettingFragment()
private var doubleBackToExitPressedOnce = false private var doubleBackToExitPressedOnce = false
private lateinit var gestureDetector: GestureDetector private lateinit var gestureDetector: GestureDetector
private val handler = Handler() private val handler = Handler()
private val delayHideMain: Long = 5000 private val delayHideMain: Long = 10000
private val delayHideSetting: Long = 10000 private val delayHideSetting: Long = 10000
lateinit var sharedPref: SharedPreferences
private var channelReversal = false
private var channelNum = true
private var versionName = ""
init {
lifecycleScope.launch(Dispatchers.IO) {
val utilsJob = async(start = CoroutineStart.LAZY) { Utils.init() }
utilsJob.start()
utilsJob.await()
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
Log.i(TAG, "onCreate") Log.i(TAG, "onCreate")
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
Request.onCreate()
Request.setRequestListener(this)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
window.decorView.systemUiVisibility = SYSTEM_UI_FLAG_HIDE_NAVIGATION window.decorView.systemUiVisibility = SYSTEM_UI_FLAG_HIDE_NAVIGATION
@@ -79,24 +53,11 @@ class MainActivity : FragmentActivity() {
.commit() .commit()
} }
gestureDetector = GestureDetector(this, GestureListener()) gestureDetector = GestureDetector(this, GestureListener())
sharedPref = getPreferences(Context.MODE_PRIVATE)
channelReversal = sharedPref.getBoolean(CHANNEL_REVERSAL, channelReversal)
channelNum = sharedPref.getBoolean(CHANNEL_NUM, channelNum)
val packageInfo = getPackageInfo()
versionName = packageInfo.versionName
val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode
} else {
packageInfo.versionCode.toLong()
}
settingFragment = SettingFragment(versionName, versionCode, channelReversal, channelNum)
} }
fun showInfoFragment(tvViewModel: TVViewModel) { fun showInfoFragment(tvViewModel: TVViewModel) {
infoFragment.show(tvViewModel) infoFragment.show(tvViewModel)
if (channelNum) { if (SP.channelNum) {
channelFragment.show(tvViewModel) channelFragment.show(tvViewModel)
} }
} }
@@ -110,7 +71,7 @@ class MainActivity : FragmentActivity() {
return return
} }
if (channelNum) { if (SP.channelNum) {
channelFragment.show(channel) channelFragment.show(channel)
} }
} }
@@ -146,7 +107,7 @@ class MainActivity : FragmentActivity() {
if (mainFragment.isHidden) { if (mainFragment.isHidden) {
mainFragment.setPosition() mainFragment.setPosition()
transaction.show(mainFragment) transaction.show(mainFragment)
keepRunnable() mainActive()
} else { } else {
transaction.hide(mainFragment) transaction.hide(mainFragment)
} }
@@ -154,11 +115,16 @@ class MainActivity : FragmentActivity() {
transaction.commit() transaction.commit()
} }
fun keepRunnable() { fun mainActive() {
handler.removeCallbacks(hideMain) handler.removeCallbacks(hideMain)
handler.postDelayed(hideMain, delayHideMain) handler.postDelayed(hideMain, delayHideMain)
} }
fun settingActive() {
handler.removeCallbacks(hideSetting)
handler.postDelayed(hideSetting, delayHideSetting)
}
private val hideMain = Runnable { private val hideMain = Runnable {
if (!mainFragment.isHidden) { if (!mainFragment.isHidden) {
supportFragmentManager.beginTransaction().hide(mainFragment).commit() supportFragmentManager.beginTransaction().hide(mainFragment).commit()
@@ -180,7 +146,7 @@ class MainActivity : FragmentActivity() {
fun fragmentReady() { fun fragmentReady() {
ready++ ready++
Log.i(TAG, "ready $ready") Log.i(TAG, "ready $ready")
if (ready == 4) { if (ready == 5) {
mainFragment.fragmentReady() mainFragment.fragmentReady()
} }
} }
@@ -233,22 +199,6 @@ class MainActivity : FragmentActivity() {
} }
} }
fun saveChannelReversal(channelReversal: Boolean) {
with(sharedPref.edit()) {
putBoolean(CHANNEL_REVERSAL, channelReversal)
apply()
}
this.channelReversal = channelReversal
}
fun saveChannelNum(channelNum: Boolean) {
with(sharedPref.edit()) {
putBoolean(CHANNEL_NUM, channelNum)
apply()
}
this.channelNum = channelNum
}
private fun showSetting() { private fun showSetting() {
if (!mainFragment.isHidden) { if (!mainFragment.isHidden) {
return return
@@ -257,8 +207,7 @@ class MainActivity : FragmentActivity() {
Log.i(TAG, "settingFragment ${settingFragment.isVisible}") Log.i(TAG, "settingFragment ${settingFragment.isVisible}")
if (!settingFragment.isVisible) { if (!settingFragment.isVisible) {
settingFragment.show(supportFragmentManager, "setting") settingFragment.show(supportFragmentManager, "setting")
handler.removeCallbacks(hideSetting) settingActive()
handler.postDelayed(hideSetting, delayHideSetting)
} else { } else {
handler.removeCallbacks(hideSetting) handler.removeCallbacks(hideSetting)
settingFragment.dismiss() settingFragment.dismiss()
@@ -273,7 +222,7 @@ class MainActivity : FragmentActivity() {
private fun channelUp() { private fun channelUp() {
if (mainFragment.isHidden) { if (mainFragment.isHidden) {
if (channelReversal) { if (SP.channelReversal) {
next() next()
return return
} }
@@ -290,7 +239,7 @@ class MainActivity : FragmentActivity() {
private fun channelDown() { private fun channelDown() {
if (mainFragment.isHidden) { if (mainFragment.isHidden) {
if (channelReversal) { if (SP.channelReversal) {
prev() prev()
return return
} }
@@ -323,7 +272,9 @@ class MainActivity : FragmentActivity() {
}, 2000) }, 2000)
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
fun onKey(keyCode: Int): Boolean {
Log.i(TAG, "keyCode $keyCode")
when (keyCode) { when (keyCode) {
KeyEvent.KEYCODE_0 -> { KeyEvent.KEYCODE_0 -> {
showChannel("0") showChannel("0")
@@ -435,6 +386,7 @@ class MainActivity : FragmentActivity() {
} }
KeyEvent.KEYCODE_DPAD_LEFT -> { KeyEvent.KEYCODE_DPAD_LEFT -> {
channelUp()
// if (mainFragment.isHidden) { // if (mainFragment.isHidden) {
// prevSource() // prevSource()
// } else { // } else {
@@ -448,6 +400,7 @@ class MainActivity : FragmentActivity() {
} }
KeyEvent.KEYCODE_DPAD_RIGHT -> { KeyEvent.KEYCODE_DPAD_RIGHT -> {
channelDown()
// if (mainFragment.isHidden) { // if (mainFragment.isHidden) {
// nextSource() // nextSource()
// } else { // } else {
@@ -459,60 +412,18 @@ class MainActivity : FragmentActivity() {
// } // }
} }
} }
return false
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (onKey(keyCode)) {
return true
}
return super.onKeyDown(keyCode, event) return super.onKeyDown(keyCode, event)
} }
private fun getPackageInfo(): PackageInfo { private fun getAppSignature() = this.appSignature
val flag = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
PackageManager.GET_SIGNATURES
} else {
PackageManager.GET_SIGNING_CERTIFICATES
}
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackageInfo(packageName, flag)
} else {
packageManager.getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(PackageManager.GET_SIGNING_CERTIFICATES.toLong())
)
}
}
private fun getAppSignature(): String {
val packageInfo = getPackageInfo()
var sign: Signature? = null
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
val signatures: Array<out Signature>? = packageInfo.signatures
if (signatures != null) {
sign = signatures[0]
}
} else {
val signingInfo: SigningInfo? = packageInfo.signingInfo
if (signingInfo != null) {
sign = signingInfo.apkContentsSigners[0]
}
}
if (sign == null) {
return ""
}
return hashSignature(sign)
}
private fun hashSignature(signature: Signature): String {
return try {
val md = MessageDigest.getInstance("MD5")
md.update(signature.toByteArray())
val digest = md.digest()
digest.let { it -> it.joinToString("") { "%02x".format(it) } }
} catch (e: Exception) {
Log.e(TAG, "Error hashing signature", e)
""
}
}
override fun onStart() { override fun onStart() {
Log.i(TAG, "onStart") Log.i(TAG, "onStart")
@@ -533,9 +444,16 @@ class MainActivity : FragmentActivity() {
handler.removeCallbacks(hideMain) handler.removeCallbacks(hideMain)
} }
companion object { override fun onDestroy() {
private const val TAG = "MainActivity" super.onDestroy()
private const val CHANNEL_REVERSAL = "channel_reversal" Request.onDestroy()
private const val CHANNEL_NUM = "channel_num" }
override fun onRequestFinished() {
fragmentReady()
}
private companion object {
const val TAG = "MainActivity"
} }
} }
@@ -1,20 +1,18 @@
package com.lizongying.mytv package com.lizongying.mytv
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lizongying.mytv.Utils.dpToPx import com.lizongying.mytv.Utils.dpToPx
import com.lizongying.mytv.Utils.getDateTimestamp import com.lizongying.mytv.api.YSP
import com.lizongying.mytv.databinding.RowBinding import com.lizongying.mytv.databinding.RowBinding
import com.lizongying.mytv.databinding.ShowBinding import com.lizongying.mytv.databinding.ShowBinding
import com.lizongying.mytv.models.TVListViewModel import com.lizongying.mytv.models.TVListViewModel
@@ -22,6 +20,7 @@ import com.lizongying.mytv.models.TVViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MainFragment : Fragment(), CardAdapter.ItemListener { class MainFragment : Fragment(), CardAdapter.ItemListener {
private var itemPosition = 0 private var itemPosition = 0
@@ -31,17 +30,10 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
private var _binding: ShowBinding? = null private var _binding: ShowBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private var request = Request()
var tvListViewModel = TVListViewModel() var tvListViewModel = TVListViewModel()
private lateinit var sharedPref: SharedPreferences
private var lastVideoUrl = "" private var lastVideoUrl = ""
private val handler = Handler(Looper.getMainLooper())
private lateinit var mUpdateProgramRunnable: UpdateProgramRunnable
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
@@ -53,8 +45,9 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
activity?.let { request.initYSP(it) } activity?.let { YSP.init(it) }
sharedPref = (activity as? MainActivity)?.sharedPref!!
itemPosition = SP.itemPosition
view?.post { view?.post {
val content = binding.content val content = binding.content
@@ -71,18 +64,40 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
tvViewModel.setItemPosition(idx2) tvViewModel.setItemPosition(idx2)
tvListViewModelCurrent.addTVViewModel(tvViewModel) tvListViewModelCurrent.addTVViewModel(tvViewModel)
tvListViewModel.addTVViewModel(tvViewModel) tvListViewModel.addTVViewModel(tvViewModel)
updateEPG(tvViewModel)
} }
tvListViewModel.maxNum.add(v.size) tvListViewModel.maxNum.add(v.size)
val adapter = CardAdapter(viewLifecycleOwner, tvListViewModelCurrent) val adapter =
CardAdapter(
itemBinding.rowItems,
this,
tvListViewModelCurrent,
)
rowList.add(itemBinding.rowItems) rowList.add(itemBinding.rowItems)
adapter.setItemListener(this) adapter.setItemListener(this)
itemBinding.rowHeader.text = k itemBinding.rowHeader.text = k
itemBinding.rowItems.tag = idx.toInt()
itemBinding.rowItems.adapter = adapter itemBinding.rowItems.adapter = adapter
itemBinding.rowItems.layoutManager = itemBinding.rowItems.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) GridLayoutManager(context, 6)
itemBinding.rowItems.layoutParams.height =
dpToPx(92 * ((tvListViewModelCurrent.size() + 6 - 1) / 6))
itemBinding.rowItems.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
(activity as MainActivity).mainActive()
}
})
val itemDecoration = context?.let { GrayOverlayItemDecoration(it) }
if (itemDecoration != null) {
itemBinding.rowItems.addItemDecoration(itemDecoration)
}
val layoutParams = itemBinding.row.layoutParams as ViewGroup.MarginLayoutParams val layoutParams = itemBinding.row.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.topMargin = dpToPx(11F) layoutParams.topMargin = dpToPx(11F)
@@ -92,15 +107,11 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
idx++ idx++
} }
mUpdateProgramRunnable = UpdateProgramRunnable()
handler.post(mUpdateProgramRunnable)
itemPosition = sharedPref.getInt(POSITION, 0)
if (itemPosition >= tvListViewModel.size()) { if (itemPosition >= tvListViewModel.size()) {
itemPosition = 0 itemPosition = 0
} }
tvListViewModel.setItemPosition(itemPosition) tvListViewModel.setItemPosition(itemPosition)
setPosition()
tvListViewModel.tvListViewModel.value?.forEach { tvViewModel -> tvListViewModel.tvListViewModel.value?.forEach { tvViewModel ->
tvViewModel.errInfo.observe(viewLifecycleOwner) { _ -> tvViewModel.errInfo.observe(viewLifecycleOwner) { _ ->
if (tvViewModel.errInfo.value != null if (tvViewModel.errInfo.value != null
@@ -128,7 +139,7 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
if (tvViewModel.pid.value != "") { if (tvViewModel.pid.value != "") {
Log.i(TAG, "request $title") Log.i(TAG, "request $title")
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
tvViewModel.let { request.fetchData(it) } tvViewModel.let { Request.fetchData(it) }
} }
(activity as? MainActivity)?.showInfoFragment(tvViewModel) (activity as? MainActivity)?.showInfoFragment(tvViewModel)
setPosition( setPosition(
@@ -136,6 +147,7 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
) )
} else { } else {
if (check(tvViewModel)) { if (check(tvViewModel)) {
// TODO lastVideoUrl
(activity as? MainActivity)?.play(tvViewModel) (activity as? MainActivity)?.play(tvViewModel)
(activity as? MainActivity)?.showInfoFragment(tvViewModel) (activity as? MainActivity)?.showInfoFragment(tvViewModel)
setPosition( setPosition(
@@ -146,7 +158,6 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
} }
} }
} }
(activity as MainActivity).fragmentReady() (activity as MainActivity).fragmentReady()
} }
} }
@@ -155,14 +166,39 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
} }
override fun onItemFocusChange(tvViewModel: TVViewModel, hasFocus: Boolean) { override fun onKey(keyCode: Int): Boolean {
if (hasFocus) { if (this.isHidden) {
tvListViewModel.setItemPositionCurrent(tvViewModel.id.value!!) when (keyCode) {
(activity as MainActivity).keepRunnable() KeyEvent.KEYCODE_DPAD_UP -> {
(activity as MainActivity).onKey(keyCode)
return true
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
(activity as MainActivity).onKey(keyCode)
return true
}
}
} }
return false
}
override fun onItemHasFocus(tvViewModel: TVViewModel) {
tvListViewModel.setItemPositionCurrent(tvViewModel.id.value!!)
val row = tvViewModel.getRowPosition()
for (i in rowList) {
if (i.tag as Int != row) {
((i as RecyclerView).adapter as CardAdapter).clear()
}
}
(activity as MainActivity).mainActive()
} }
override fun onItemClicked(tvViewModel: TVViewModel) { override fun onItemClicked(tvViewModel: TVViewModel) {
Log.i(TAG, "onItemClicked")
if (this.isHidden) { if (this.isHidden) {
(activity as? MainActivity)?.switchMainFragment() (activity as? MainActivity)?.switchMainFragment()
return return
@@ -178,10 +214,8 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
fun setPosition() { fun setPosition() {
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition) val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
Log.i(TAG, "tvViewModel $tvViewModel")
Log.i(TAG, "rowList ${rowList.size}")
rowList[tvViewModel!!.getRowPosition()].post { rowList[tvViewModel!!.getRowPosition()].post {
((rowList[tvViewModel.getRowPosition()] as RecyclerView).layoutManager as LinearLayoutManager).findViewByPosition( ((rowList[tvViewModel.getRowPosition()] as RecyclerView).layoutManager as GridLayoutManager).findViewByPosition(
tvViewModel.getItemPosition() tvViewModel.getItemPosition()
)?.requestFocus() )?.requestFocus()
} }
@@ -189,7 +223,7 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
fun setPosition(rowPosition: Int, itemPosition: Int) { fun setPosition(rowPosition: Int, itemPosition: Int) {
rowList[rowPosition].post { rowList[rowPosition].post {
((rowList[rowPosition] as RecyclerView).layoutManager as LinearLayoutManager).findViewByPosition( ((rowList[rowPosition] as RecyclerView).layoutManager as GridLayoutManager).findViewByPosition(
itemPosition itemPosition
)?.requestFocus() )?.requestFocus()
} }
@@ -250,30 +284,37 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
} }
} }
fun updateProgram(tvViewModel: TVViewModel) { private fun updateEPG(tvViewModel: TVViewModel) {
val timestamp = getDateTimestamp() if (tvViewModel.getTV().channel == "港澳台") {
if (timestamp - tvViewModel.programUpdateTime > 60) { Request.fetchFEPG(tvViewModel)
if (tvViewModel.program.value!!.isEmpty()) { } else {
tvViewModel.programUpdateTime = timestamp Request.fetchYEPG(tvViewModel)
request.fetchProgram(tvViewModel)
} else {
if (tvViewModel.program.value!!.last().et - timestamp < 600) {
tvViewModel.programUpdateTime = timestamp
request.fetchProgram(tvViewModel)
}
}
} }
} }
inner class UpdateProgramRunnable : Runnable { fun shouldHasFocus(tvModel: TVViewModel):Boolean {
override fun run() { return tvModel == tvListViewModel.getTVViewModel(itemPosition)
tvListViewModel.tvListViewModel.value?.filter { it.programId.value != null && it.programId.value != "" } }
?.forEach { tvViewModel ->
updateProgram( override fun onHiddenChanged(hidden: Boolean) {
tvViewModel super.onHiddenChanged(hidden)
) if (!hidden) {
val tvModel = tvListViewModel.getTVViewModel(itemPosition)
val rowPosition = tvModel?.getRowPosition()
val itemPosition = tvModel?.getItemPosition()
Log.i(TAG, "toPosition $rowPosition $itemPosition")
for (i in rowList) {
if (i.tag as Int == rowPosition) {
((i as RecyclerView).adapter as CardAdapter).toPosition(itemPosition!!)
break
} }
handler.postDelayed(this, 60000) }
} else {
view?.post {
// for (i in rowList) {
// ((i as RecyclerView).adapter as CardAdapter).visiable = false
// }
}
} }
} }
@@ -285,19 +326,13 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
override fun onStop() { override fun onStop() {
Log.i(TAG, "onStop") Log.i(TAG, "onStop")
super.onStop() super.onStop()
with(sharedPref.edit()) { SP.itemPosition = itemPosition
putInt(POSITION, itemPosition) Log.i(TAG, "$POSITION $itemPosition saved")
apply()
}
Log.i(TAG, "$POSITION saved")
} }
override fun onDestroy() { override fun onDestroy() {
Log.i(TAG, "onDestroy") Log.i(TAG, "onDestroy")
super.onDestroy() super.onDestroy()
if (::mUpdateProgramRunnable.isInitialized) {
handler.removeCallbacks(mUpdateProgramRunnable)
}
} }
override fun onDestroyView() { override fun onDestroyView() {
@@ -3,6 +3,8 @@ package com.lizongying.mytv
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
@@ -15,23 +17,39 @@ import androidx.media3.common.VideoSize
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import com.google.android.exoplayer2.SimpleExoPlayer
import com.lizongying.mytv.databinding.PlayerBinding import com.lizongying.mytv.databinding.PlayerBinding
import com.lizongying.mytv.models.TVViewModel import com.lizongying.mytv.models.TVViewModel
class PlayerFragment : Fragment() { class PlayerFragment : Fragment(), SurfaceHolder.Callback {
private var _binding: PlayerBinding? = null private var _binding: PlayerBinding? = null
private var playerView: PlayerView? = null private var playerView: PlayerView? = null
private var tvViewModel: TVViewModel? = null private var tvViewModel: TVViewModel? = null
private val aspectRatio = 16f / 9f private val aspectRatio = 16f / 9f
private lateinit var surfaceView: SurfaceView
private lateinit var surfaceHolder: SurfaceHolder
private var exoPlayer: SimpleExoPlayer? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
_binding = PlayerBinding.inflate(inflater, container, false) _binding = PlayerBinding.inflate(inflater, container, false)
playerView = _binding!!.playerView
if (Utils.isTmallDevice()) {
_binding!!.playerView.visibility = View.GONE
surfaceView = _binding!!.surfaceView
surfaceHolder = surfaceView.holder
surfaceHolder.addCallback(this)
} else {
_binding!!.surfaceView.visibility = View.GONE
playerView = _binding!!.playerView
}
playerView?.viewTreeObserver?.addOnGlobalLayoutListener(object : playerView?.viewTreeObserver?.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener { ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() { override fun onGlobalLayout() {
@@ -78,6 +96,10 @@ class PlayerFragment : Fragment() {
setMediaItem(MediaItem.fromUri(tvViewModel.getVideoUrlCurrent())) setMediaItem(MediaItem.fromUri(tvViewModel.getVideoUrlCurrent()))
prepare() prepare()
} }
exoPlayer?.run {
setMediaItem(com.google.android.exoplayer2.MediaItem.fromUri(tvViewModel.getVideoUrlCurrent()))
prepare()
}
} }
override fun onStart() { override fun onStart() {
@@ -87,8 +109,11 @@ class PlayerFragment : Fragment() {
Log.i(TAG, "replay") Log.i(TAG, "replay")
playerView!!.player?.prepare() playerView!!.player?.prepare()
playerView!!.player?.play() playerView!!.player?.play()
} else { }
Log.i(TAG, "playing") if (exoPlayer?.isPlaying == false) {
Log.i(TAG, "replay")
exoPlayer?.prepare()
exoPlayer?.play()
} }
} }
@@ -102,6 +127,9 @@ class PlayerFragment : Fragment() {
if (playerView != null && playerView!!.player?.isPlaying == true) { if (playerView != null && playerView!!.player?.isPlaying == true) {
playerView!!.player?.stop() playerView!!.player?.stop()
} }
if (exoPlayer?.isPlaying == true) {
exoPlayer?.stop()
}
} }
override fun onDestroy() { override fun onDestroy() {
@@ -109,6 +137,7 @@ class PlayerFragment : Fragment() {
if (playerView != null) { if (playerView != null) {
playerView!!.player?.release() playerView!!.player?.release()
} }
exoPlayer?.release()
} }
override fun onDestroyView() { override fun onDestroyView() {
@@ -119,4 +148,16 @@ class PlayerFragment : Fragment() {
companion object { companion object {
private const val TAG = "PlaybackVideoFragment" private const val TAG = "PlaybackVideoFragment"
} }
override fun surfaceCreated(holder: SurfaceHolder) {
exoPlayer = SimpleExoPlayer.Builder(requireContext()).build()
exoPlayer?.setVideoSurfaceHolder(surfaceHolder)
exoPlayer?.playWhenReady = true
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
}
} }
+366 -125
View File
@@ -1,6 +1,5 @@
package com.lizongying.mytv package com.lizongying.mytv
import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Base64 import android.util.Base64
@@ -9,9 +8,14 @@ import com.lizongying.mytv.Utils.getDateFormat
import com.lizongying.mytv.api.ApiClient import com.lizongying.mytv.api.ApiClient
import com.lizongying.mytv.api.Auth import com.lizongying.mytv.api.Auth
import com.lizongying.mytv.api.AuthRequest import com.lizongying.mytv.api.AuthRequest
import com.lizongying.mytv.api.FAuth
import com.lizongying.mytv.api.FAuthService
import com.lizongying.mytv.api.FEPG
import com.lizongying.mytv.api.Info import com.lizongying.mytv.api.Info
import com.lizongying.mytv.api.InfoV2
import com.lizongying.mytv.api.LiveInfo import com.lizongying.mytv.api.LiveInfo
import com.lizongying.mytv.api.LiveInfoRequest import com.lizongying.mytv.api.LiveInfoRequest
import com.lizongying.mytv.api.Token
import com.lizongying.mytv.api.YSP import com.lizongying.mytv.api.YSP
import com.lizongying.mytv.api.YSPApiService import com.lizongying.mytv.api.YSPApiService
import com.lizongying.mytv.api.YSPBtraceService import com.lizongying.mytv.api.YSPBtraceService
@@ -26,17 +30,22 @@ import retrofit2.Response
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
import kotlin.math.max
class Request { object Request {
private const val TAG = "Request"
private var yspTokenService: YSPTokenService = ApiClient().yspTokenService private var yspTokenService: YSPTokenService = ApiClient().yspTokenService
private var yspApiService: YSPApiService = ApiClient().yspApiService private var yspApiService: YSPApiService = ApiClient().yspApiService
private var yspBtraceService: YSPBtraceService = ApiClient().yspBtraceService private var yspBtraceService: YSPBtraceService = ApiClient().yspBtraceService
private var yspProtoService: YSPProtoService = ApiClient().yspProtoService private var yspProtoService: YSPProtoService = ApiClient().yspProtoService
private var ysp: YSP? = null private var fAuthService: FAuthService = ApiClient().fAuthService
private var token = "" private var token = ""
private var tokenFH = ""
private var needAuth = false
// TODO onDestroy
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private lateinit var btraceRunnable: BtraceRunnable private lateinit var btraceRunnable: BtraceRunnable
private var tokenRunnable: TokenRunnable = TokenRunnable() private var tokenRunnable: TokenRunnable = TokenRunnable()
@@ -45,26 +54,43 @@ class Request {
private val input = private val input =
"""{"mver":"1","subver":"1.2","host":"www.yangshipin.cn/#/tv/home?pid=","referer":"","canvas":"YSPANGLE(Apple,ANGLEMetalRenderer:AppleM1Pro,UnspecifiedVersion)"}""".toByteArray() """{"mver":"1","subver":"1.2","host":"www.yangshipin.cn/#/tv/home?pid=","referer":"","canvas":"YSPANGLE(Apple,ANGLEMetalRenderer:AppleM1Pro,UnspecifiedVersion)"}""".toByteArray()
init { private var listener: RequestListener? = null
handler.post(tokenRunnable)
private var initRetryTimes = 0
private var initRetryMaxTimes = 1
fun onCreate() {
Log.i(TAG, "onCreate")
fetchInfoV2()
} }
fun initYSP(context: Context) { fun onDestroy() {
ysp = YSP(context) Log.i(TAG, "onDestroy")
handler.removeCallbacks(tokenRunnable)
} }
var call: Call<LiveInfo>? = null private var call: Call<LiveInfo>? = null
var callAuth: Call<Auth>? = null private var callAuth: Call<Auth>? = null
private var callInfo: Call<Info>? = null
private var callFAuth: Call<FAuth>? = null
private var callPage: Call<pageModel.Response>? = null
private fun cancelCall() {
call?.cancel()
callAuth?.cancel()
callInfo?.cancel()
callFAuth?.cancel()
callPage?.cancel()
}
private fun fetchAuth(tvModel: TVViewModel, cookie: String) { private fun fetchAuth(tvModel: TVViewModel, cookie: String) {
callAuth?.cancel() cancelCall()
val title = tvModel.title.value val title = tvModel.title.value
val data = ysp?.getAuthData(tvModel) val data = YSP.getAuthData(tvModel)
val request = data?.let { AuthRequest(it) } val request = AuthRequest(data)
callAuth = request?.let { yspApiService.getAuth("guid=${ysp?.getGuid()}; $cookie", it) } callAuth = request.let { yspApiService.getAuth("guid=${YSP.getGuid()}; $cookie", it) }
callAuth?.enqueue(object : Callback<Auth> { callAuth?.enqueue(object : Callback<Auth> {
override fun onResponse(call: Call<Auth>, response: Response<Auth>) { override fun onResponse(call: Call<Auth>, response: Response<Auth>) {
if (response.isSuccessful) { if (response.isSuccessful) {
@@ -72,19 +98,19 @@ class Request {
if (liveInfo?.data?.token != null) { if (liveInfo?.data?.token != null) {
Log.i(TAG, "token ${liveInfo.data.token}") Log.i(TAG, "token ${liveInfo.data.token}")
ysp?.token = liveInfo.data.token YSP.token = liveInfo.data.token
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
} else { } else {
Log.e(TAG, "$title token error") Log.e(TAG, "$title token error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) { if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++ tvModel.retryTimes++
if (tvModel.getTV().needToken) { if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) { if (tvModel.tokenYSPRetryTimes == tvModel.tokenYSPRetryMaxTimes) {
if (!tvModel.getTV().mustToken) { if (!tvModel.getTV().mustToken) {
fetchAuth(tvModel, cookie) fetchAuth(tvModel, cookie)
} }
} else { } else {
token = "" tvModel.needGetToken = true
fetchAuth(tvModel) fetchAuth(tvModel)
} }
} else { } else {
@@ -97,12 +123,12 @@ class Request {
if (tvModel.retryTimes < tvModel.retryMaxTimes) { if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++ tvModel.retryTimes++
if (tvModel.getTV().needToken) { if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) { if (tvModel.tokenYSPRetryTimes == tvModel.tokenYSPRetryMaxTimes) {
if (!tvModel.getTV().mustToken) { if (!tvModel.getTV().mustToken) {
fetchAuth(tvModel, cookie) fetchAuth(tvModel, cookie)
} }
} else { } else {
token = "" tvModel.needGetToken = true
fetchAuth(tvModel) fetchAuth(tvModel)
} }
} else { } else {
@@ -117,12 +143,12 @@ class Request {
if (tvModel.retryTimes < tvModel.retryMaxTimes) { if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++ tvModel.retryTimes++
if (tvModel.getTV().needToken) { if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) { if (tvModel.tokenYSPRetryTimes == tvModel.tokenYSPRetryMaxTimes) {
if (!tvModel.getTV().mustToken) { if (!tvModel.getTV().mustToken) {
fetchVideo(tvModel, cookie) fetchAuth(tvModel, cookie)
} }
} else { } else {
token = "" tvModel.needGetToken = true
fetchAuth(tvModel) fetchAuth(tvModel)
} }
} else { } else {
@@ -133,8 +159,9 @@ class Request {
}) })
} }
fun fetchVideo(tvModel: TVViewModel, cookie: String) { private fun fetchVideo(tvModel: TVViewModel, cookie: String) {
call?.cancel() cancelCall()
if (::btraceRunnable.isInitialized) { if (::btraceRunnable.isInitialized) {
handler.removeCallbacks(btraceRunnable) handler.removeCallbacks(btraceRunnable)
} }
@@ -142,9 +169,15 @@ class Request {
val title = tvModel.title.value val title = tvModel.title.value
tvModel.seq = 0 tvModel.seq = 0
val data = ysp?.switch(tvModel) val data = YSP.switch(tvModel)
val request = data?.let { LiveInfoRequest(it) } val request = LiveInfoRequest(data)
call = request?.let { yspApiService.getLiveInfo("guid=${ysp?.getGuid()}; $cookie", ysp!!.token, it) } call = request.let {
yspApiService.getLiveInfo(
"guid=${YSP.getGuid()}; $cookie",
YSP.token,
it
)
}
call?.enqueue(object : Callback<LiveInfo> { call?.enqueue(object : Callback<LiveInfo> {
override fun onResponse(call: Call<LiveInfo>, response: Response<LiveInfo>) { override fun onResponse(call: Call<LiveInfo>, response: Response<LiveInfo>) {
@@ -178,16 +211,19 @@ class Request {
if (tvModel.retryTimes < tvModel.retryMaxTimes) { if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++ tvModel.retryTimes++
if (tvModel.getTV().needToken) { if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) { if (tvModel.tokenYSPRetryTimes == tvModel.tokenYSPRetryMaxTimes) {
if (!tvModel.getTV().mustToken) { if (!tvModel.getTV().mustToken) {
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
} }
} else { } else {
token = "" tvModel.needGetToken = true
fetchVideo(tvModel) fetchVideo(tvModel)
// fetchAuth(tvModel)
} }
} else { } else {
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
} }
} }
} }
@@ -200,16 +236,19 @@ class Request {
if (tvModel.retryTimes < tvModel.retryMaxTimes) { if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++ tvModel.retryTimes++
if (tvModel.getTV().needToken) { if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) { if (tvModel.tokenYSPRetryTimes == tvModel.tokenYSPRetryMaxTimes) {
if (!tvModel.getTV().mustToken) { if (!tvModel.getTV().mustToken) {
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
} }
} else { } else {
token = "" tvModel.needGetToken = true
fetchVideo(tvModel) fetchVideo(tvModel)
// fetchAuth(tvModel)
} }
} else { } else {
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
} }
} }
} }
@@ -219,32 +258,35 @@ class Request {
if (tvModel.retryTimes < tvModel.retryMaxTimes) { if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++ tvModel.retryTimes++
if (tvModel.getTV().needToken) { if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) { if (tvModel.tokenYSPRetryTimes == tvModel.tokenYSPRetryMaxTimes) {
if (!tvModel.getTV().mustToken) { if (!tvModel.getTV().mustToken) {
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
} }
} else { } else {
token = "" tvModel.needGetToken = true
fetchVideo(tvModel) fetchVideo(tvModel)
// fetchAuth(tvModel)
} }
} else { } else {
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
} }
} }
} }
} }
override fun onFailure(call: Call<LiveInfo>, t: Throwable) { override fun onFailure(call: Call<LiveInfo>, t: Throwable) {
Log.e(TAG, "$title request error $t") Log.e(TAG, "$title fetchVideo request error $t")
if (tvModel.retryTimes < tvModel.retryMaxTimes) { if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++ tvModel.retryTimes++
if (tvModel.getTV().needToken) { if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) { if (tvModel.tokenYSPRetryTimes == tvModel.tokenYSPRetryMaxTimes) {
if (!tvModel.getTV().mustToken) { if (!tvModel.getTV().mustToken) {
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
} }
} else { } else {
token = "" tvModel.needGetToken = true
fetchVideo(tvModel) fetchVideo(tvModel)
} }
} else { } else {
@@ -255,37 +297,31 @@ class Request {
}) })
} }
fun fetchAuth(tvModel: TVViewModel) { private fun fetchAuth(tvModel: TVViewModel) {
if (token == "") { cancelCall()
yspTokenService.getInfo() if (tvModel.needGetToken) {
.enqueue(object : Callback<Info> { callInfo = yspTokenService.getInfo("")
override fun onResponse(call: Call<Info>, response: Response<Info>) { callInfo?.enqueue(object : Callback<Info> {
if (response.isSuccessful) { override fun onResponse(call: Call<Info>, response: Response<Info>) {
token = response.body()?.data?.token!! if (response.isSuccessful && response.body()?.data?.token != null) {
Log.i(TAG, "info success $token") token = response.body()?.data?.token!!
val cookie = Log.i(TAG, "info success $token")
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109;yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token" tvModel.needGetToken = false
fetchAuth(tvModel, cookie) tvModel.tokenYSPRetryTimes = 0
} else { val cookie =
Log.e(TAG, "info status error") "versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109;yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
if (tvModel.tokenRetryTimes < tvModel.tokenRetryMaxTimes) { fetchAuth(tvModel, cookie)
tvModel.tokenRetryTimes++ } else if (response.code() == 304) {
fetchAuth(tvModel) tvModel.needGetToken = false
} else { tvModel.tokenYSPRetryTimes = 0
if (!tvModel.getTV().mustToken) { val cookie =
val cookie = "versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109" fetchVideo(tvModel, cookie)
fetchAuth(tvModel, cookie) } else {
} Log.e(TAG, "info status error")
} if (tvModel.tokenYSPRetryTimes < tvModel.tokenYSPRetryMaxTimes) {
} tvModel.tokenYSPRetryTimes++
} fetchAuth(tvModel)
override fun onFailure(call: Call<Info>, t: Throwable) {
Log.e(TAG, "info request error $t")
if (tvModel.tokenRetryTimes < tvModel.tokenRetryMaxTimes) {
tvModel.tokenRetryTimes++
fetchVideo(tvModel)
} else { } else {
if (!tvModel.getTV().mustToken) { if (!tvModel.getTV().mustToken) {
val cookie = val cookie =
@@ -294,7 +330,22 @@ class Request {
} }
} }
} }
}) }
override fun onFailure(call: Call<Info>, t: Throwable) {
Log.e(TAG, "info request error $t")
if (tvModel.tokenYSPRetryTimes < tvModel.tokenYSPRetryMaxTimes) {
tvModel.tokenYSPRetryTimes++
fetchVideo(tvModel)
} else {
if (!tvModel.getTV().mustToken) {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109"
fetchAuth(tvModel, cookie)
}
}
}
})
} else { } else {
val cookie = val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109;yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token" "versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109;yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
@@ -302,36 +353,31 @@ class Request {
} }
} }
fun fetchVideo(tvModel: TVViewModel) { private fun fetchVideo(tvModel: TVViewModel) {
if (token == "") { cancelCall()
yspTokenService.getInfo() Log.d(TAG, "fetchVideo")
.enqueue(object : Callback<Info> { if (tvModel.needGetToken) {
override fun onResponse(call: Call<Info>, response: Response<Info>) { callInfo = yspTokenService.getInfo("")
if (response.isSuccessful) { callInfo?.enqueue(object : Callback<Info> {
token = response.body()?.data?.token!! override fun onResponse(call: Call<Info>, response: Response<Info>) {
Log.i(TAG, "info success $token") if (response.isSuccessful && response.body()?.data?.token != null) {
val cookie = token = response.body()?.data?.token!!
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109;yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token" Log.i(TAG, "info success $token")
fetchVideo(tvModel, cookie) tvModel.needGetToken = false
} else { tvModel.tokenYSPRetryTimes = 0
Log.e(TAG, "info status error") val cookie =
if (tvModel.tokenRetryTimes < tvModel.tokenRetryMaxTimes) { "versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
tvModel.tokenRetryTimes++ fetchVideo(tvModel, cookie)
fetchVideo(tvModel) } else if (response.code() == 304) {
} else { tvModel.needGetToken = false
if (!tvModel.getTV().mustToken) { tvModel.tokenYSPRetryTimes = 0
val cookie = val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109" "versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
} } else {
} Log.e(TAG, "info status error")
} if (tvModel.tokenYSPRetryTimes < tvModel.tokenYSPRetryMaxTimes) {
} tvModel.tokenYSPRetryTimes++
override fun onFailure(call: Call<Info>, t: Throwable) {
Log.e(TAG, "info request error $t")
if (tvModel.tokenRetryTimes < tvModel.tokenRetryMaxTimes) {
tvModel.tokenRetryTimes++
fetchVideo(tvModel) fetchVideo(tvModel)
} else { } else {
if (!tvModel.getTV().mustToken) { if (!tvModel.getTV().mustToken) {
@@ -341,52 +387,217 @@ class Request {
} }
} }
} }
}) }
override fun onFailure(call: Call<Info>, t: Throwable) {
Log.e(TAG, "info request error $t")
if (tvModel.tokenYSPRetryTimes < tvModel.tokenYSPRetryMaxTimes) {
tvModel.tokenYSPRetryTimes++
fetchVideo(tvModel)
} else {
if (!tvModel.getTV().mustToken) {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109"
fetchVideo(tvModel, cookie)
}
}
}
})
} else { } else {
val cookie = val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109;yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token" "versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
} }
} }
private fun fetchFAuth(tvModel: TVViewModel) {
cancelCall()
val title = tvModel.title.value
var qa = "HD"
if (tokenFH != "") {
qa = "FHD"
}
callFAuth = fAuthService.getAuth(tokenFH, tvModel.getTV().pid, qa)
callFAuth?.enqueue(object : Callback<FAuth> {
override fun onResponse(call: Call<FAuth>, response: Response<FAuth>) {
if (response.isSuccessful && response.body()?.data?.live_url != null) {
val url = response.body()?.data?.live_url!!
// Log.d(TAG, "$title url $url")
tvModel.addVideoUrl(url)
tvModel.allReady()
tvModel.tokenFHRetryTimes = 0
} else {
Log.e(TAG, "auth status error ${response.code()}")
if (tvModel.tokenFHRetryTimes < tvModel.tokenFHRetryMaxTimes) {
tvModel.tokenFHRetryTimes++
fetchFAuth(tvModel)
}
}
}
override fun onFailure(call: Call<FAuth>, t: Throwable) {
Log.e(TAG, "auth request error $t")
if (tvModel.tokenFHRetryTimes < tvModel.tokenFHRetryMaxTimes) {
tvModel.tokenFHRetryTimes++
fetchFAuth(tvModel)
}
}
})
}
fun fetchData(tvModel: TVViewModel) { fun fetchData(tvModel: TVViewModel) {
if (tvModel.getTV().channel == "港澳台") {
fetchFAuth(tvModel)
return
}
if (tvModel.getTV().needToken) { if (tvModel.getTV().needToken) {
// fetchVideo(tvModel) if (needAuth) {
fetchAuth(tvModel) fetchAuth(tvModel)
} else {
fetchVideo(tvModel)
}
} else { } else {
val cookie = val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205" "versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205"
// fetchVideo(tvModel, cookie) if (needAuth) {
fetchAuth(tvModel, cookie) fetchAuth(tvModel, cookie)
} else {
fetchVideo(tvModel, cookie)
}
} }
} }
inner class TokenRunnable : Runnable { class TokenRunnable : Runnable {
override fun run() { override fun run() {
fetchToken() // fetchToken()
handler.postDelayed(this, 600000)
} }
} }
fun fetchToken() { private fun fetchInfoV2() {
yspTokenService.getInfo() yspTokenService.getInfoV2()
.enqueue(object : Callback<Info> { .enqueue(object : Callback<InfoV2> {
override fun onResponse(call: Call<Info>, response: Response<Info>) { override fun onResponse(call: Call<InfoV2>, response: Response<InfoV2>) {
if (response.isSuccessful) { if (response.isSuccessful) {
token = response.body()?.data?.token!! val t = response.body()?.t
Log.i(TAG, "info success $token") val f = response.body()?.f
val e = response.body()?.e
val c = response.body()?.c
if (!t.isNullOrEmpty()) {
token = t
Log.i(TAG, "token success $token")
if (e != null) {
handler.postDelayed(
tokenRunnable,
max(30 * 60 * 1000, (e - 1500) * 1000).toLong()
)
} else {
Log.e(TAG, "e empty")
handler.postDelayed(
tokenRunnable,
30 * 60 * 1000
)
}
} else {
Log.e(TAG, "token empty")
handler.postDelayed(
tokenRunnable,
30 * 60 * 1000
)
}
if (!f.isNullOrEmpty()) {
tokenFH = f
Log.i(TAG, "tokenFH success $tokenFH")
}
if (c != null) {
Utils.setBetween(c * 1000L)
Log.i(TAG, "current time $c")
}
listener?.onRequestFinished()
} else { } else {
Log.e(TAG, "token status error") Log.e(TAG, "token status error")
handler.postDelayed(
tokenRunnable,
30 * 60 * 1000
)
if (initRetryTimes < initRetryMaxTimes) {
initRetryTimes++
fetchInfoV2()
} else {
listener?.onRequestFinished()
}
} }
} }
override fun onFailure(call: Call<Info>, t: Throwable) { override fun onFailure(call: Call<InfoV2>, t: Throwable) {
Log.e(TAG, "token request error $t") Log.e(TAG, "token request error $t")
handler.postDelayed(
tokenRunnable,
30 * 60 * 1000
)
if (initRetryTimes < initRetryMaxTimes) {
initRetryTimes++
fetchInfoV2()
} else {
listener?.onRequestFinished()
}
} }
}) })
} }
inner class BtraceRunnable(private val tvModel: TVViewModel) : Runnable { fun fetchToken() {
yspTokenService.getToken(token)
.enqueue(object : Callback<Token> {
override fun onResponse(call: Call<Token>, response: Response<Token>) {
if (response.isSuccessful) {
val t = response.body()?.t
val e = response.body()?.e
if (!t.isNullOrEmpty()) {
token = t
Log.e(TAG, "token success $token")
if (e != null) {
handler.postDelayed(
tokenRunnable,
max(30 * 60 * 1000, (e - 1500) * 1000).toLong()
)
} else {
Log.e(TAG, "e empty")
handler.postDelayed(
tokenRunnable,
30 * 60 * 1000
)
}
} else {
Log.e(TAG, "token empty")
handler.postDelayed(
tokenRunnable,
30 * 60 * 1000
)
}
} else {
Log.e(TAG, "token status error")
handler.postDelayed(
tokenRunnable,
30 * 60 * 1000
)
}
}
override fun onFailure(call: Call<Token>, t: Throwable) {
Log.e(TAG, "token request error $t")
handler.postDelayed(
tokenRunnable,
30 * 60 * 1000
)
}
})
}
class BtraceRunnable(private val tvModel: TVViewModel) : Runnable {
override fun run() { override fun run() {
fetchBtrace(tvModel) fetchBtrace(tvModel)
handler.postDelayed(this, 60000) handler.postDelayed(this, 60000)
@@ -396,11 +607,11 @@ class Request {
fun fetchBtrace(tvModel: TVViewModel) { fun fetchBtrace(tvModel: TVViewModel) {
val title = tvModel.title.value val title = tvModel.title.value
val guid = ysp?.getGuid()!! val guid = YSP.getGuid()
val pid = tvModel.pid.value!! val pid = tvModel.pid.value!!
val sid = tvModel.sid.value!! val sid = tvModel.sid.value!!
yspBtraceService.kvcollect( yspBtraceService.kvcollect(
c_timestamp = ysp?.generateGuid()!!, c_timestamp = YSP.generateGuid(),
guid = guid, guid = guid,
c_guid = guid, c_guid = guid,
prog = sid, prog = sid,
@@ -408,7 +619,7 @@ class Request {
fpid = pid, fpid = pid,
livepid = pid, livepid = pid,
sUrl = "https://www.yangshipin.cn/#/tv/home?pid=$pid", sUrl = "https://www.yangshipin.cn/#/tv/home?pid=$pid",
playno = ysp?.getRand()!!, playno = YSP.getRand(),
ftime = getDateFormat("yyyy-MM-dd HH:mm:ss"), ftime = getDateFormat("yyyy-MM-dd HH:mm:ss"),
seq = tvModel.seq.toString(), seq = tvModel.seq.toString(),
) )
@@ -429,7 +640,8 @@ class Request {
} }
fun fetchPage() { fun fetchPage() {
yspProtoService.getPage().enqueue(object : Callback<pageModel.Response> { callPage = yspProtoService.getPage()
callPage?.enqueue(object : Callback<pageModel.Response> {
override fun onResponse( override fun onResponse(
call: Call<pageModel.Response>, call: Call<pageModel.Response>,
response: Response<pageModel.Response> response: Response<pageModel.Response>
@@ -462,7 +674,7 @@ class Request {
}) })
} }
fun fetchProgram(tvViewModel: TVViewModel) { fun fetchYEPG(tvViewModel: TVViewModel) {
val title = tvViewModel.title.value val title = tvViewModel.title.value
yspProtoService.getProgram(tvViewModel.programId.value!!, getDateFormat("yyyyMMdd")) yspProtoService.getProgram(tvViewModel.programId.value!!, getDateFormat("yyyyMMdd"))
.enqueue(object : Callback<epgProgramModel.Response> { .enqueue(object : Callback<epgProgramModel.Response> {
@@ -473,7 +685,7 @@ class Request {
if (response.isSuccessful) { if (response.isSuccessful) {
val program = response.body() val program = response.body()
if (program != null) { if (program != null) {
tvViewModel.addProgram(program.dataListList) tvViewModel.addYEPG(program.dataListList)
Log.d(TAG, "$title program ${program.dataListList.size}") Log.d(TAG, "$title program ${program.dataListList.size}")
} }
} else { } else {
@@ -487,6 +699,31 @@ class Request {
}) })
} }
fun fetchFEPG(tvViewModel: TVViewModel) {
val title = tvViewModel.title.value
fAuthService.getEPG(tvViewModel.pid.value!!, getDateFormat("yyyyMMdd"))
.enqueue(object : Callback<List<FEPG>> {
override fun onResponse(
call: Call<List<FEPG>>,
response: Response<List<FEPG>>
) {
if (response.isSuccessful) {
val program = response.body()
if (program != null) {
tvViewModel.addFEPG(program)
Log.d(TAG, "$title program ${program.size}")
}
} else {
Log.w(TAG, "$title program error")
}
}
override fun onFailure(call: Call<List<FEPG>>, t: Throwable) {
Log.e(TAG, "$title program request failed $t")
}
})
}
private fun encryptTripleDES(key: ByteArray, iv: ByteArray): String { private fun encryptTripleDES(key: ByteArray, iv: ByteArray): String {
return try { return try {
val keySpec = SecretKeySpec(key, "DESede") val keySpec = SecretKeySpec(key, "DESede")
@@ -500,7 +737,11 @@ class Request {
} }
} }
companion object { interface RequestListener {
private const val TAG = "Request" fun onRequestFinished()
}
fun setRequestListener(listener: RequestListener) {
this.listener = listener
} }
} }
@@ -0,0 +1,48 @@
package com.lizongying.mytv
import android.content.Context
import android.content.SharedPreferences
object SP {
// Name of the sp file TODO Should use a meaningful name and do migrations
private const val SP_FILE_NAME = "MainActivity"
// If Change channel with up and down in reversed order or not
private const val KEY_CHANNEL_REVERSAL = "channel_reversal"
// If use channel num to select channel or not
private const val KEY_CHANNEL_NUM = "channel_num"
// If start app on device boot or not
private const val KEY_BOOT_STARTUP = "boot_startup"
// Position in list of the selected channel item
private const val KEY_POSITION = "position"
// guid
private const val KEY_GUID = "guid"
private lateinit var sp: SharedPreferences
/**
* The method must be invoked as early as possible(At least before using the keys)
*/
fun init(context: Context) {
sp = context.getSharedPreferences(SP_FILE_NAME, Context.MODE_PRIVATE)
}
var channelReversal: Boolean
get() = sp.getBoolean(KEY_CHANNEL_REVERSAL, false)
set(value) = sp.edit().putBoolean(KEY_CHANNEL_REVERSAL, value).apply()
var channelNum: Boolean
get() = sp.getBoolean(KEY_CHANNEL_NUM, true)
set(value) = sp.edit().putBoolean(KEY_CHANNEL_NUM, value).apply()
var bootStartup: Boolean
get() = sp.getBoolean(KEY_BOOT_STARTUP, false)
set(value) = sp.edit().putBoolean(KEY_BOOT_STARTUP, value).apply()
var itemPosition: Int
get() = sp.getInt(KEY_POSITION, 0)
set(value) = sp.edit().putInt(KEY_POSITION, value).apply()
var guid: String
get() = sp.getString(KEY_GUID, "") ?: ""
set(value) = sp.edit().putString(KEY_GUID, value).apply()
}
@@ -1,22 +1,14 @@
package com.lizongying.mytv package com.lizongying.mytv
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.RequiresApi
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.lizongying.mytv.databinding.DialogBinding import com.lizongying.mytv.databinding.DialogBinding
class SettingFragment( class SettingFragment : DialogFragment() {
private val versionName: String,
private val versionCode: Long,
private val channelReversal: Boolean,
private val channelNum: Boolean,
) :
DialogFragment() {
private var _binding: DialogBinding? = null private var _binding: DialogBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@@ -33,24 +25,37 @@ class SettingFragment(
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
val context = requireContext() // It‘s safe to get context here.
_binding = DialogBinding.inflate(inflater, container, false) _binding = DialogBinding.inflate(inflater, container, false)
_binding?.version?.text = binding.version.text =
"当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/" "当前版本: ${context.appVersionName}\n获取最新: https://github.com/lizongying/my-tv/releases/"
val switchChannelReversal = _binding?.switchChannelReversal binding.switchChannelReversal.run {
switchChannelReversal?.isChecked = channelReversal isChecked = SP.channelReversal
switchChannelReversal?.setOnCheckedChangeListener { _, isChecked -> setOnCheckedChangeListener { _, isChecked ->
(activity as MainActivity).saveChannelReversal(isChecked) SP.channelReversal = isChecked
(activity as MainActivity).settingActive()
}
} }
val switchChannelNum = _binding?.switchChannelNum binding.switchChannelNum.run {
switchChannelNum?.isChecked = channelNum isChecked = SP.channelNum
switchChannelNum?.setOnCheckedChangeListener { _, isChecked -> setOnCheckedChangeListener { _, isChecked ->
(activity as MainActivity).saveChannelNum(isChecked) SP.channelNum = isChecked
(activity as MainActivity).settingActive()
}
} }
updateManager = UpdateManager(context, this, versionCode) binding.switchBootStartup.run {
_binding?.checkVersion?.setOnClickListener(OnClickListenerCheckVersion(updateManager)) isChecked = SP.bootStartup
setOnCheckedChangeListener { _, isChecked ->
SP.bootStartup = isChecked
(activity as MainActivity).settingActive()
}
}
updateManager = UpdateManager(context, this, context.appVersionCode)
binding.checkVersion.setOnClickListener(OnClickListenerCheckVersion(updateManager))
return binding.root return binding.root
} }
+1 -1
View File
@@ -9,7 +9,7 @@ data class TV(
var videoUrl: List<String>, var videoUrl: List<String>,
var videoIndex: Int = 0, var videoIndex: Int = 0,
var channel: String = "", var channel: String = "",
var logo: String = "", var logo: Any = "",
var pid: String = "", var pid: String = "",
var sid: String = "", var sid: String = "",
var programId: String = "", var programId: String = "",
File diff suppressed because it is too large Load Diff
@@ -23,7 +23,7 @@ import java.io.File
class UpdateManager( class UpdateManager(
private var context: Context?, private var context: Context,
private var settingFragment: SettingFragment, private var settingFragment: SettingFragment,
private var versionCode: Long private var versionCode: Long
) : ) :
@@ -67,7 +67,7 @@ class UpdateManager(
val apkFileName = "my-tv-${release.data.versionName}.apk" val apkFileName = "my-tv-${release.data.versionName}.apk"
Log.i(TAG, "apkFileName $apkFileName") Log.i(TAG, "apkFileName $apkFileName")
val downloadManager = val downloadManager =
context!!.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = Request(Uri.parse(release.data.downloadUrl)) val request = Request(Uri.parse(release.data.downloadUrl))
Log.i(TAG, "url ${Uri.parse(release.data.downloadUrl)}") Log.i(TAG, "url ${Uri.parse(release.data.downloadUrl)}")
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, apkFileName) request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, apkFileName)
@@ -77,22 +77,22 @@ class UpdateManager(
// 获取下载任务的引用 // 获取下载任务的引用
val downloadReference = downloadManager.enqueue(request) val downloadReference = downloadManager.enqueue(request)
downloadReceiver = DownloadReceiver(context!!, apkFileName, downloadReference) downloadReceiver = DownloadReceiver(context, apkFileName, downloadReference)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context!!.registerReceiver( context.registerReceiver(
downloadReceiver, downloadReceiver,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
Context.RECEIVER_NOT_EXPORTED, Context.RECEIVER_NOT_EXPORTED,
) )
} else { } else {
context!!.registerReceiver( context.registerReceiver(
downloadReceiver, downloadReceiver,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
) )
} }
getDownloadProgress(context!!, downloadReference) { progress -> getDownloadProgress(context, downloadReference) { progress ->
println("Download progress: $progress%") println("Download progress: $progress%")
} }
} }
@@ -182,7 +182,7 @@ class UpdateManager(
fun destroy() { fun destroy() {
if (downloadReceiver != null) { if (downloadReceiver != null) {
context!!.unregisterReceiver(downloadReceiver) context.unregisterReceiver(downloadReceiver)
Log.i(TAG, "destroy downloadReceiver") Log.i(TAG, "destroy downloadReceiver")
} }
} }
+6 -46
View File
@@ -1,13 +1,12 @@
package com.lizongying.mytv package com.lizongying.mytv
import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.os.Build
import android.util.TypedValue import android.util.TypedValue
import com.google.gson.Gson import com.google.gson.Gson
import com.lizongying.mytv.api.TimeResponse import com.lizongying.mytv.api.TimeResponse
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException import java.io.IOException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@@ -27,6 +26,10 @@ object Utils {
return (System.currentTimeMillis() - between) / 1000 return (System.currentTimeMillis() - between) / 1000
} }
fun setBetween(currentTimeMillis: Long) {
between = System.currentTimeMillis() - currentTimeMillis
}
suspend fun init() { suspend fun init() {
var currentTimeMillis: Long = 0 var currentTimeMillis: Long = 0
try { try {
@@ -74,48 +77,5 @@ object Utils {
).toInt() ).toInt()
} }
/** fun isTmallDevice() = Build.MANUFACTURER.equals("Tmall", ignoreCase = true)
* 获取可读写的目录
*
* @param context 应用环境信息
*
* @return 可读写的目录
*
*/
fun getAppDirectory(context: Context): File {
return context.filesDir
}
/**
* 更新channels.json
*
* @param context 应用环境信息
*
* @return 无
*
* @throws IOException 网络请求失败
*/
fun updateChannel(context: Context) {
val client = okhttp3.OkHttpClient()
val request = okhttp3.Request.Builder().url(getServerUrl(context)).build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val body = response.body()
//覆盖channels.json
val file = File(getAppDirectory(context), "channels.json")
if (!file.exists()) {
file.createNewFile()
}
file.writeText(body!!.string())
}
}
/**
* 从res/values/server.xml获取服务器地址
* @param context 应用环境信息
* @return 服务器地址
*/
private fun getServerUrl(context: Context): String {
return context.resources.getString(R.string.server_url)
}
} }
@@ -1,12 +1,14 @@
package com.lizongying.mytv.api package com.lizongying.mytv.api
import android.os.Build
import android.util.Log
import okhttp3.ConnectionSpec
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.TlsVersion
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.protobuf.ProtoConverterFactory import retrofit2.converter.protobuf.ProtoConverterFactory
import java.net.InetSocketAddress
import java.net.Proxy
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
@@ -18,7 +20,7 @@ class ApiClient {
private val devUrl = "http://10.0.2.2:8081/" private val devUrl = "http://10.0.2.2:8081/"
private val protoUrl = "https://capi.yangshipin.cn/" private val protoUrl = "https://capi.yangshipin.cn/"
private val traceUrl = "https://btrace.yangshipin.cn/" private val traceUrl = "https://btrace.yangshipin.cn/"
private val fUrl = "https://m.fengshows.com/"
private var okHttpClient = getUnsafeOkHttpClient() private var okHttpClient = getUnsafeOkHttpClient()
@@ -40,7 +42,7 @@ class ApiClient {
val releaseService: ReleaseService by lazy { val releaseService: ReleaseService by lazy {
Retrofit.Builder() Retrofit.Builder()
.baseUrl(devUrl) .baseUrl(myUrl)
.client(okHttpClient) .client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build().create(ReleaseService::class.java) .build().create(ReleaseService::class.java)
@@ -62,6 +64,44 @@ class ApiClient {
.build().create(YSPBtraceService::class.java) .build().create(YSPBtraceService::class.java)
} }
val fAuthService: FAuthService by lazy {
Retrofit.Builder()
.baseUrl(fUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build().create(FAuthService::class.java)
}
private fun enableTls12OnPreLollipop(client: OkHttpClient.Builder): OkHttpClient.Builder {
if (Build.VERSION.SDK_INT >= 16 && Build.VERSION.SDK_INT < 22) {
try {
val sc = SSLContext.getInstance("TLSv1.2")
sc.init(null, null, null)
// a more robust version is to pass a custom X509TrustManager
// as the second parameter and make checkServerTrusted to accept your server.
// Credits: https://github.com/square/okhttp/issues/2372#issuecomment-1774955225
client.sslSocketFactory(Tls12SocketFactory(sc.socketFactory))
val cs = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2)
.build()
val specs: MutableList<ConnectionSpec> = ArrayList()
specs.add(cs)
specs.add(ConnectionSpec.COMPATIBLE_TLS)
specs.add(ConnectionSpec.CLEARTEXT)
client.connectionSpecs(specs)
} catch (exc: java.lang.Exception) {
Log.e("OkHttpTLSCompat", "Error while setting TLS 1.2", exc)
}
}
return client
}
private fun getUnsafeOkHttpClient(): OkHttpClient { private fun getUnsafeOkHttpClient(): OkHttpClient {
try { try {
val trustAllCerts: Array<TrustManager> = arrayOf( val trustAllCerts: Array<TrustManager> = arrayOf(
@@ -87,11 +127,12 @@ class ApiClient {
val sslContext = SSLContext.getInstance("SSL") val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, java.security.SecureRandom()) sslContext.init(null, trustAllCerts, java.security.SecureRandom())
return OkHttpClient.Builder() val builder = OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager) .sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager)
.hostnameVerifier { _, _ -> true } .hostnameVerifier { _, _ -> true }
.dns(DnsCache()) .dns(DnsCache())
.build()
return enableTls12OnPreLollipop(builder).build()
} catch (e: Exception) { } catch (e: Exception) {
throw RuntimeException(e) throw RuntimeException(e)
@@ -0,0 +1,10 @@
package com.lizongying.mytv.api
data class FAuth(
val data: Data,
) {
data class Data(
val live_url: String,
)
}
@@ -0,0 +1,23 @@
package com.lizongying.mytv.api
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Path
import retrofit2.http.Query
interface FAuthService {
@GET("api/v3/hub/live/auth-url")
fun getAuth(
@Header("token") token: String,
@Query("live_id") live_id: String = "",
@Query("live_qa") live_qa: String = "",
): Call<FAuth>
@GET("api/v3/live/{liveId}/resources")
fun getEPG(
@Path("liveId") liveId: String,
@Query("date") date: String,
@Query("dir") dir: String = "asc"
): Call<List<FEPG>>
}
@@ -0,0 +1,7 @@
package com.lizongying.mytv.api
data class FEPG(
val title: String,
val event_time: String,
)
@@ -10,6 +10,18 @@ data class Info(
) )
} }
data class Token(
val e: Int?,
val t: String?,
)
data class InfoV2(
val f: String?,
val t: String?,
val e: Int?,
val c: Int?,
)
data class Release( data class Release(
val code: Int?, val code: Int?,
val msg: String?, val msg: String?,
@@ -0,0 +1,74 @@
package com.lizongying.mytv.api
import java.io.IOException
import java.net.InetAddress
import java.net.Socket
import java.net.UnknownHostException
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
/**
* Enables TLS v1.2 when creating SSLSockets.
*
*
* For some reason, android supports TLS v1.2 from API 16, but enables it by
* default only from API 20.
* @link https://developer.android.com/reference/javax/net/ssl/SSLSocket.html
* @see SSLSocketFactory
*/
class Tls12SocketFactory(val delegate: SSLSocketFactory) : SSLSocketFactory() {
override fun getDefaultCipherSuites(): Array<String> {
return delegate.defaultCipherSuites
}
override fun getSupportedCipherSuites(): Array<String> {
return delegate.supportedCipherSuites
}
@Throws(IOException::class)
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
return patch(delegate.createSocket(s, host, port, autoClose))
}
@Throws(IOException::class, UnknownHostException::class)
override fun createSocket(host: String, port: Int): Socket {
return patch(delegate.createSocket(host, port))
}
@Throws(IOException::class, UnknownHostException::class)
override fun createSocket(
host: String,
port: Int,
localHost: InetAddress,
localPort: Int
): Socket {
return patch(delegate.createSocket(host, port, localHost, localPort))
}
@Throws(IOException::class)
override fun createSocket(host: InetAddress, port: Int): Socket {
return patch(delegate.createSocket(host, port))
}
@Throws(IOException::class)
override fun createSocket(
address: InetAddress,
port: Int,
localAddress: InetAddress,
localPort: Int
): Socket {
return patch(delegate.createSocket(address, port, localAddress, localPort))
}
private fun patch(s: Socket): Socket {
if (s is SSLSocket) {
s.enabledProtocols = TLS_V12_ONLY
}
return s
}
companion object {
private val TLS_V12_ONLY = arrayOf("TLSv1.2")
}
}
@@ -1,15 +1,17 @@
package com.lizongying.mytv.api package com.lizongying.mytv.api
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import com.lizongying.mytv.Encryptor import com.lizongying.mytv.Encryptor
import com.lizongying.mytv.MainActivity import com.lizongying.mytv.MainActivity
import com.lizongying.mytv.SP
import com.lizongying.mytv.Utils.getDateTimestamp import com.lizongying.mytv.Utils.getDateTimestamp
import com.lizongying.mytv.models.TVViewModel import com.lizongying.mytv.models.TVViewModel
import kotlin.math.floor import kotlin.math.floor
import kotlin.random.Random import kotlin.random.Random
class YSP(var context: Context) { object YSP {
private const val TAG = "YSP"
private var cnlid = "" private var cnlid = ""
private var livepid = "" private var livepid = ""
@@ -53,17 +55,10 @@ class YSP(var context: Context) {
private var appid = "ysp_pc" private var appid = "ysp_pc"
var token = "" var token = ""
private var encryptor: Encryptor? = null private var encryptor = Encryptor()
private lateinit var sharedPref: SharedPreferences
init {
if (context is MainActivity) {
encryptor = Encryptor()
encryptor!!.init(context)
sharedPref = (context as MainActivity).sharedPref
}
fun init(context: Context) {
encryptor.init(context)
guid = getGuid() guid = getGuid()
} }
@@ -81,7 +76,7 @@ class YSP(var context: Context) {
timeStr = getTimeStr() timeStr = getTimeStr()
cKey = cKey =
encryptor!!.encrypt(cnlid, timeStr, appVer, guid, platform) encryptor.encrypt(cnlid, timeStr, appVer, guid, platform)
signature = getSignature() signature = getSignature()
return """{"cnlid":"$cnlid","livepid":"$livepid","stream":"$stream","guid":"$guid","cKey":"$cKey","adjust":$adjust,"sphttps":"$sphttps","platform":"$platform","cmd":"$cmd","encryptVer":"$encryptVer","dtype":"$dtype","devid":"$devid","otype":"$otype","appVer":"$appVer","app_version":"$appVersion","rand_str":"$randStr","channel":"$channel","defn":"$defn","signature":"$signature"}""" return """{"cnlid":"$cnlid","livepid":"$livepid","stream":"$stream","guid":"$guid","cKey":"$cKey","adjust":$adjust,"sphttps":"$sphttps","platform":"$platform","cmd":"$cmd","encryptVer":"$encryptVer","dtype":"$dtype","devid":"$devid","otype":"$otype","appVer":"$appVer","app_version":"$appVersion","rand_str":"$randStr","channel":"$channel","defn":"$defn","signature":"$signature"}"""
} }
@@ -110,23 +105,17 @@ class YSP(var context: Context) {
} }
fun getGuid(): String { fun getGuid(): String {
var guid = sharedPref.getString("guid", "") var guid = SP.guid
if (guid == null || guid.length < 18) { if (guid.length < 18) {
guid = generateGuid() guid = generateGuid()
with(sharedPref.edit()) { SP.guid = guid
putString("guid", guid)
apply()
}
} }
return guid return guid
} }
private fun newGuid(): String { private fun newGuid(): String {
guid = generateGuid() guid = generateGuid()
with(sharedPref.edit()) { SP.guid = guid
putString("guid", guid)
apply()
}
return guid return guid
} }
@@ -143,18 +132,14 @@ class YSP(var context: Context) {
private fun getSignature(): String { private fun getSignature(): String {
val e = val e =
"adjust=${adjust}&appVer=${appVer}&app_version=$appVersion&cKey=$cKey&channel=$channel&cmd=$cmd&cnlid=$cnlid&defn=${defn}&devid=${devid}&dtype=${dtype}&encryptVer=${encryptVer}&guid=${guid}&livepid=${livepid}&otype=${otype}&platform=${platform}&rand_str=${randStr}&sphttps=${sphttps}&stream=${stream}".toByteArray() "adjust=${adjust}&appVer=${appVer}&app_version=$appVersion&cKey=$cKey&channel=$channel&cmd=$cmd&cnlid=$cnlid&defn=${defn}&devid=${devid}&dtype=${dtype}&encryptVer=${encryptVer}&guid=${guid}&livepid=${livepid}&otype=${otype}&platform=${platform}&rand_str=${randStr}&sphttps=${sphttps}&stream=${stream}".toByteArray()
val hashedData = encryptor?.hash(e) ?: return "" val hashedData = encryptor.hash(e) ?: return ""
return hashedData.let { it -> it.joinToString("") { "%02x".format(it) } } return hashedData.let { it -> it.joinToString("") { "%02x".format(it) } }
} }
private fun getAuthSignature(): String { private fun getAuthSignature(): String {
val e = val e =
"appid=${appid}&guid=${guid}&pid=${livepid}&rand_str=${randStr}".toByteArray() "appid=${appid}&guid=${guid}&pid=${livepid}&rand_str=${randStr}".toByteArray()
val hashedData = encryptor?.hash2(e) ?: return "" val hashedData = encryptor.hash2(e) ?: return ""
return hashedData.let { it -> it.joinToString("") { "%02x".format(it) } } return hashedData.let { it -> it.joinToString("") { "%02x".format(it) } }
} }
companion object {
private const val TAG = "YSP"
}
} }
@@ -2,10 +2,21 @@ package com.lizongying.mytv.api
import retrofit2.Call import retrofit2.Call
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Query
interface YSPTokenService { interface YSPTokenService {
@GET("my-tv/v1/info") @GET("my-tv/v1/info")
fun getInfo( fun getInfo(
@Query("token") token: String = "",
): Call<Info> ): Call<Info>
@GET("my-tv/v2/token")
fun getToken(
@Query("token") token: String = "",
): Call<Token>
@GET("my-tv/v2/info")
fun getInfoV2(
): Call<InfoV2>
} }
@@ -0,0 +1,7 @@
package com.lizongying.mytv.models
data class EPG(
val title: String,
val beginTime: Int,
)
@@ -11,8 +11,10 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.exoplayer.hls.HlsMediaSource import androidx.media3.exoplayer.hls.HlsMediaSource
import com.lizongying.mytv.TV import com.lizongying.mytv.TV
import com.lizongying.mytv.Utils.getDateTimestamp import com.lizongying.mytv.api.FEPG
import com.lizongying.mytv.proto.Ysp.cn.yangshipin.omstv.common.proto.programModel.Program import com.lizongying.mytv.proto.Ysp.cn.yangshipin.omstv.common.proto.programModel.Program
import java.text.SimpleDateFormat
import java.util.TimeZone
class TVViewModel(private var tv: TV) : ViewModel() { class TVViewModel(private var tv: TV) : ViewModel() {
@@ -20,10 +22,13 @@ class TVViewModel(private var tv: TV) : ViewModel() {
private var itemPosition: Int = 0 private var itemPosition: Int = 0
var retryTimes = 0 var retryTimes = 0
var tokenRetryTimes = 0
var retryMaxTimes = 8 var retryMaxTimes = 8
var tokenRetryMaxTimes = 2 var tokenYSPRetryTimes = 0
var programUpdateTime: Long = 0 var tokenYSPRetryMaxTimes = 0
var tokenFHRetryTimes = 0
var tokenFHRetryMaxTimes = 2
var needGetToken = false
private val _errInfo = MutableLiveData<String>() private val _errInfo = MutableLiveData<String>()
val errInfo: LiveData<String> val errInfo: LiveData<String>
@@ -33,9 +38,9 @@ class TVViewModel(private var tv: TV) : ViewModel() {
val programId: LiveData<String> val programId: LiveData<String>
get() = _programId get() = _programId
private var _program = MutableLiveData<MutableList<Program>>() private var _epg = MutableLiveData<MutableList<EPG>>()
val program: LiveData<MutableList<Program>> val epg: LiveData<MutableList<EPG>>
get() = _program get() = _epg
private val _id = MutableLiveData<Int>() private val _id = MutableLiveData<Int>()
val id: LiveData<Int> val id: LiveData<Int>
@@ -53,8 +58,8 @@ class TVViewModel(private var tv: TV) : ViewModel() {
val videoIndex: LiveData<Int> val videoIndex: LiveData<Int>
get() = _videoIndex get() = _videoIndex
private val _logo = MutableLiveData<String>() private val _logo = MutableLiveData<Any>()
val logo: LiveData<String> val logo: LiveData<Any>
get() = _logo get() = _logo
private val _pid = MutableLiveData<String>() private val _pid = MutableLiveData<String>()
@@ -73,11 +78,8 @@ class TVViewModel(private var tv: TV) : ViewModel() {
val ready: LiveData<Boolean> val ready: LiveData<Boolean>
get() = _ready get() = _ready
private var mMinimumLoadableRetryCount = 5
var seq = 0 var seq = 0
fun addVideoUrl(url: String) { fun addVideoUrl(url: String) {
if (_videoUrl.value?.isNotEmpty() == true) { if (_videoUrl.value?.isNotEmpty() == true) {
if (_videoUrl.value!!.last().contains("cctv.cn")) { if (_videoUrl.value!!.last().contains("cctv.cn")) {
@@ -114,10 +116,6 @@ class TVViewModel(private var tv: TV) : ViewModel() {
_videoIndex.value = videoIndex _videoIndex.value = videoIndex
} }
fun setLogo(url: String) {
_logo.value = url
}
init { init {
_id.value = tv.id _id.value = tv.id
_title.value = tv.title _title.value = tv.title
@@ -127,10 +125,8 @@ class TVViewModel(private var tv: TV) : ViewModel() {
_programId.value = tv.programId _programId.value = tv.programId
_pid.value = tv.pid _pid.value = tv.pid
_sid.value = tv.sid _sid.value = tv.sid
_program.value = mutableListOf()
} }
fun getRowPosition(): Int { fun getRowPosition(): Int {
return rowPosition return rowPosition
} }
@@ -159,46 +155,22 @@ class TVViewModel(private var tv: TV) : ViewModel() {
return tv return tv
} }
fun getProgramOne(): Program? { fun addYEPG(p: MutableList<Program>) {
val programNew = _epg.value = p.map { EPG(it.name, it.st.toInt()) }.toMutableList()
(_program.value?.filter { it.et > getDateTimestamp() })?.toMutableList()
if (programNew != null && _program.value != programNew) {
_program.value = programNew
}
if (_program.value!!.isEmpty()) {
return null
}
return _program.value?.first()
} }
fun addProgram(p: MutableList<Program>) { private fun formatFTime(s: String): Int {
val timestamp = getDateTimestamp() val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
// after now & not empty & different val date = dateFormat.parse(s.substring(0, 19))
val p1 = (p.filter { it.et > timestamp }).toMutableList() if (date != null) {
if (p1.isEmpty() || _program.value == p1) { return (date.time / 1000).toInt()
return
}
if (_program.value!!.isEmpty()) {
_program.value = p1
} else {
_program.value =
((_program.value?.filter { it.et > timestamp && it.st < p1.first().st })?.plus(
p1
))?.toMutableList()
} }
return 0
} }
fun addFEPG(p: List<FEPG>) {
private var mHeaders: Map<String, String>? = mapOf() _epg.value = p.map { EPG(it.title, formatFTime(it.event_time)) }.toMutableList()
fun setHeaders(headers: Map<String, String>) {
mHeaders = headers
}
fun setMinimumLoadableRetryCount(minimumLoadableRetryCount: Int) {
mMinimumLoadableRetryCount = minimumLoadableRetryCount
} }
/** /**
@@ -207,7 +179,7 @@ class TVViewModel(private var tv: TV) : ViewModel() {
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
fun buildSource(): HlsMediaSource { fun buildSource(): HlsMediaSource {
val httpDataSource = DefaultHttpDataSource.Factory() val httpDataSource = DefaultHttpDataSource.Factory()
mHeaders?.let { httpDataSource.setDefaultRequestProperties(it) } // mHeaders?.let { httpDataSource.setDefaultRequestProperties(it) }
return HlsMediaSource.Factory(httpDataSource).createMediaSource( return HlsMediaSource.Factory(httpDataSource).createMediaSource(
MediaItem.fromUri( MediaItem.fromUri(
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

-1
View File
@@ -13,7 +13,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_gravity="end"
android:layout_marginTop="0dp" android:layout_marginTop="0dp"
android:text="99"
android:textColor="#FFEEEEEE" android:textColor="#FFEEEEEE"
android:textStyle="bold" android:textStyle="bold"
android:textSize="50sp" /> android:textSize="50sp" />
+6
View File
@@ -64,6 +64,12 @@
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<Switch
android:id="@+id/switch_boot_startup"
android:text="@string/title_boot_startup"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>
<ImageView <ImageView
android:layout_width="300dp" android:layout_width="300dp"
+8
View File
@@ -7,6 +7,14 @@
android:background="@color/black" android:background="@color/black"
android:keepScreenOn="true"> android:keepScreenOn="true">
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
app:resize_mode="fill"
app:use_controller="false" />
<androidx.media3.ui.PlayerView <androidx.media3.ui.PlayerView
android:id="@+id/player_view" android:id="@+id/player_view"
android:layout_width="match_parent" android:layout_width="match_parent"
+1 -1
View File
@@ -2,7 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/row" android:id="@+id/row"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
+63 -63
View File
@@ -2,7 +2,7 @@
{ {
"id": 0, "id": 0,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/d57905b93540bd15f0c48230dbbbff7ee0d645ff539e38866e2d15c8b9f7dfcd.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/d57905b93540bd15f0c48230dbbbff7ee0d645ff539e38866e2d15c8b9f7dfcd.png?imageMogr2/format/webp",
"pid": "600001859", "pid": "600001859",
"sid": "2000210103", "sid": "2000210103",
@@ -17,7 +17,7 @@
{ {
"id": 1, "id": 1,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/20115388de0207131af17eac86c33049b95d69eaff064e55653a1b941810a006.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/20115388de0207131af17eac86c33049b95d69eaff064e55653a1b941810a006.png?imageMogr2/format/webp",
"pid": "600001800", "pid": "600001800",
"sid": "2000203603", "sid": "2000203603",
@@ -32,7 +32,7 @@
{ {
"id": 2, "id": 2,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/7b7a65c712450da3deb6ca66fbacf4f9aee00d3f20bd80eafb5ada01ec63eb3a.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/7b7a65c712450da3deb6ca66fbacf4f9aee00d3f20bd80eafb5ada01ec63eb3a.png?imageMogr2/format/webp",
"pid": "600001801", "pid": "600001801",
"sid": "2000203803", "sid": "2000203803",
@@ -47,7 +47,7 @@
{ {
"id": 3, "id": 3,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/f357e58fdbcc076a3d65e1f958c942b2e14f14342c60736ceed98b092d35356a.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/f357e58fdbcc076a3d65e1f958c942b2e14f14342c60736ceed98b092d35356a.png?imageMogr2/format/webp",
"pid": "600001814", "pid": "600001814",
"sid": "2000204803", "sid": "2000204803",
@@ -63,7 +63,7 @@
{ {
"id": 4, "id": 4,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/0a6a7138952675983a3d854df7688557b286d59aa06166edae51506f9204d655.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/0a6a7138952675983a3d854df7688557b286d59aa06166edae51506f9204d655.png?imageMogr2/format/webp",
"pid": "600001818", "pid": "600001818",
"sid": "2000205103", "sid": "2000205103",
@@ -78,7 +78,7 @@
{ {
"id": 5, "id": 5,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/741515efda91f03f455df8a7da4ee11fa9329139c276435cf0a9e2af398d5bf2.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/741515efda91f03f455df8a7da4ee11fa9329139c276435cf0a9e2af398d5bf2.png?imageMogr2/format/webp",
"pid": "600001802", "pid": "600001802",
"sid": "2013693901", "sid": "2013693901",
@@ -93,7 +93,7 @@
{ {
"id": 6, "id": 6,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/b29af94e295ebdf646cefb68122c429b9cd921f498ca20d2d8070252536f9ff9.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/b29af94e295ebdf646cefb68122c429b9cd921f498ca20d2d8070252536f9ff9.png?imageMogr2/format/webp",
"pid": "600004092", "pid": "600004092",
"sid": "2000510003", "sid": "2000510003",
@@ -108,7 +108,7 @@
{ {
"id": 7, "id": 7,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/ad51de94426a0ba039e6dd6a8534ea98ecc813a6176bde87b4f18cc34d6d7590.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/ad51de94426a0ba039e6dd6a8534ea98ecc813a6176bde87b4f18cc34d6d7590.png?imageMogr2/format/webp",
"pid": "600001803", "pid": "600001803",
"sid": "2000203903", "sid": "2000203903",
@@ -123,14 +123,14 @@
{ {
"id": 8, "id": 8,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/2ed1b4deeca179d5db806bb941790f82eb92a1b7299c1c38fe027f95a5caee5e.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/2ed1b4deeca179d5db806bb941790f82eb92a1b7299c1c38fe027f95a5caee5e.png?imageMogr2/format/webp",
"pid": "600004078", "pid": "600004078",
"sid": "2000499403", "sid": "2000499403",
"programId": "600004078", "programId": "600004078",
"needToken": true, "needToken": true,
"mustToken": false, "mustToken": false,
"title": "CCTV9 记录", "title": "CCTV9 纪录",
"videoUrl": [ "videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226197/index.m3u8" "http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226197/index.m3u8"
] ]
@@ -138,7 +138,7 @@
{ {
"id": 9, "id": 9,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/aa6157ec65188cd41826e5a2f088c3d6d153205f5f6428258d12c59999e221aa.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/aa6157ec65188cd41826e5a2f088c3d6d153205f5f6428258d12c59999e221aa.png?imageMogr2/format/webp",
"pid": "600001805", "pid": "600001805",
"sid": "2000203503", "sid": "2000203503",
@@ -153,7 +153,7 @@
{ {
"id": 10, "id": 10,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/ed12ed7c7a1034dae4350011fe039284c5d5a836506b28c9e32e3c75299625c0.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/ed12ed7c7a1034dae4350011fe039284c5d5a836506b28c9e32e3c75299625c0.png?imageMogr2/format/webp",
"pid": "600001806", "pid": "600001806",
"sid": "2000204103", "sid": "2000204103",
@@ -168,7 +168,7 @@
{ {
"id": 11, "id": 11,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/484083cffaa40df7e659565e8cb4d1cc740158a185512114167aa21fa0c59240.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/484083cffaa40df7e659565e8cb4d1cc740158a185512114167aa21fa0c59240.png?imageMogr2/format/webp",
"pid": "600001807", "pid": "600001807",
"sid": "2000202603", "sid": "2000202603",
@@ -183,7 +183,7 @@
{ {
"id": 12, "id": 12,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/266da7b43c03e2312186b4a999e0f060e8f15b10d2cc2c9aa32171819254cf1a.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/266da7b43c03e2312186b4a999e0f060e8f15b10d2cc2c9aa32171819254cf1a.png?imageMogr2/format/webp",
"pid": "600001811", "pid": "600001811",
"sid": "2000204603", "sid": "2000204603",
@@ -199,7 +199,7 @@
{ {
"id": 13, "id": 13,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/af6b603896938dc346fbb16abfc63c12cba54b0ec9d18770a15d347d115f12d5.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/af6b603896938dc346fbb16abfc63c12cba54b0ec9d18770a15d347d115f12d5.png?imageMogr2/format/webp",
"pid": "600001809", "pid": "600001809",
"sid": "2000204403", "sid": "2000204403",
@@ -214,7 +214,7 @@
{ {
"id": 14, "id": 14,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/2ceee92188ef684efe0d8b90839c4f3ad450d179dc64d59beff417059453af47.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/2ceee92188ef684efe0d8b90839c4f3ad450d179dc64d59beff417059453af47.png?imageMogr2/format/webp",
"pid": "600001815", "pid": "600001815",
"sid": "2000205003", "sid": "2000205003",
@@ -229,7 +229,7 @@
{ {
"id": 15, "id": 15,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/53793fa7bacd3a93ff6dc5d2758418985e1f952a316c335d663b572d8bdcd74d.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/53793fa7bacd3a93ff6dc5d2758418985e1f952a316c335d663b572d8bdcd74d.png?imageMogr2/format/webp",
"pid": "600098637", "pid": "600098637",
"sid": "2012375003", "sid": "2012375003",
@@ -245,7 +245,7 @@
{ {
"id": 16, "id": 16,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/ddef563072f8bad2bea5b9e52674cb7b4ed50efb20c26e61994dfbdf05c1e3c0.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/ddef563072f8bad2bea5b9e52674cb7b4ed50efb20c26e61994dfbdf05c1e3c0.png?imageMogr2/format/webp",
"pid": "600001810", "pid": "600001810",
"sid": "2000204203", "sid": "2000204203",
@@ -260,7 +260,7 @@
{ {
"id": 17, "id": 17,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/649ad76a90bfef55b05db9fe52e006487280f619089099d5dc971e387fc6eff0.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/649ad76a90bfef55b05db9fe52e006487280f619089099d5dc971e387fc6eff0.png?imageMogr2/format/webp",
"pid": "600001817", "pid": "600001817",
"sid": "2000204503", "sid": "2000204503",
@@ -275,7 +275,7 @@
{ {
"id": 18, "id": 18,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/3e9d06fd7244d950df5838750f1c6ac3456e172b51caca2c16d2282125b111e8.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/3e9d06fd7244d950df5838750f1c6ac3456e172b51caca2c16d2282125b111e8.png?imageMogr2/format/webp",
"pid": "600002264", "pid": "600002264",
"sid": "2000266303", "sid": "2000266303",
@@ -290,7 +290,7 @@
{ {
"id": 19, "id": 19,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "", "logo": "",
"pid": "600156816", "pid": "600156816",
"sid": "2020603421", "sid": "2020603421",
@@ -305,7 +305,7 @@
{ {
"id": 20, "id": 20,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/4d549e53e6d0f632d5a633d1945280797b153e588f919221a07faa869812cc89.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/4d549e53e6d0f632d5a633d1945280797b153e588f919221a07faa869812cc89.png?imageMogr2/format/webp",
"pid": "600099658", "pid": "600099658",
"sid": "2012513603", "sid": "2012513603",
@@ -320,7 +320,7 @@
{ {
"id": 21, "id": 21,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/a556bd7d93ce65e18f243a8892b5604f4faa994a4897315914216a710a706208.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/a556bd7d93ce65e18f243a8892b5604f4faa994a4897315914216a710a706208.png?imageMogr2/format/webp",
"pid": "600099655", "pid": "600099655",
"sid": "2012514403", "sid": "2012514403",
@@ -335,7 +335,7 @@
{ {
"id": 22, "id": 22,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/5661bd04fecdb6e899f801147a22ab5d3a475bf2b62e30aec2c0023190ebc9b1.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/5661bd04fecdb6e899f801147a22ab5d3a475bf2b62e30aec2c0023190ebc9b1.png?imageMogr2/format/webp",
"pid": "600099620", "pid": "600099620",
"sid": "2012511203", "sid": "2012511203",
@@ -350,7 +350,7 @@
{ {
"id": 23, "id": 23,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/bb3c6c9e145d698137f5bb64a582021a01b51344b929003630eb769ea65832a9.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/bb3c6c9e145d698137f5bb64a582021a01b51344b929003630eb769ea65832a9.png?imageMogr2/format/webp",
"pid": "600099637", "pid": "600099637",
"sid": "2012513303", "sid": "2012513303",
@@ -365,7 +365,7 @@
{ {
"id": 24, "id": 24,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/bbf1d024c5228b8dd128b0e3cb1717d173fab4ee84c3a4c8a57b1a215362ca3b.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/bbf1d024c5228b8dd128b0e3cb1717d173fab4ee84c3a4c8a57b1a215362ca3b.png?imageMogr2/format/webp",
"pid": "600099660", "pid": "600099660",
"sid": "2012514103", "sid": "2012514103",
@@ -380,7 +380,7 @@
{ {
"id": 25, "id": 25,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/4c6b6a6d3839889f34d33db3c2f80233b26b74d3489b393487635f8704e70796.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/4c6b6a6d3839889f34d33db3c2f80233b26b74d3489b393487635f8704e70796.png?imageMogr2/format/webp",
"pid": "600099649", "pid": "600099649",
"sid": "2012513403", "sid": "2012513403",
@@ -395,7 +395,7 @@
{ {
"id": 26, "id": 26,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/cd1e2bb52b06a991de168733e5ff0f1d85adc8042d40c8f393f723543e5dd08a.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/cd1e2bb52b06a991de168733e5ff0f1d85adc8042d40c8f393f723543e5dd08a.png?imageMogr2/format/webp",
"pid": "600099636", "pid": "600099636",
"sid": "2012514203", "sid": "2012514203",
@@ -410,7 +410,7 @@
{ {
"id": 27, "id": 27,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/cdd1b31ede7a5ad049ed53d9a072422f829e72dd062ed2c19e077fdd01699071.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/cdd1b31ede7a5ad049ed53d9a072422f829e72dd062ed2c19e077fdd01699071.png?imageMogr2/format/webp",
"pid": "600099659", "pid": "600099659",
"sid": "2012512503", "sid": "2012512503",
@@ -425,7 +425,7 @@
{ {
"id": 28, "id": 28,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/fa28955ce8b2539d728bf4c6a13a46ff57ad76eae46627f7bcfb1ed8a613d3fc.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/fa28955ce8b2539d728bf4c6a13a46ff57ad76eae46627f7bcfb1ed8a613d3fc.png?imageMogr2/format/webp",
"pid": "600099650", "pid": "600099650",
"sid": "2012513903", "sid": "2012513903",
@@ -440,7 +440,7 @@
{ {
"id": 29, "id": 29,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/14ac5ce40482cacd3d4b37435222bfe86af2b452a2f04ecbfc1d13d76edd7c57.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/14ac5ce40482cacd3d4b37435222bfe86af2b452a2f04ecbfc1d13d76edd7c57.png?imageMogr2/format/webp",
"pid": "600099653", "pid": "600099653",
"sid": "2012513803", "sid": "2012513803",
@@ -455,7 +455,7 @@
{ {
"id": 30, "id": 30,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/10e14a92478011aa6c3c8562e62127f3b1908e29fcd78e4b2b24b9e6d3ec2fbc.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/10e14a92478011aa6c3c8562e62127f3b1908e29fcd78e4b2b24b9e6d3ec2fbc.png?imageMogr2/format/webp",
"pid": "600099652", "pid": "600099652",
"sid": "2012513703", "sid": "2012513703",
@@ -470,7 +470,7 @@
{ {
"id": 31, "id": 31,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/244d72c0eb1615ed7d51c2f5db5a67f306aa3f58c05bc2d34de3aa7e956dc8c9.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/244d72c0eb1615ed7d51c2f5db5a67f306aa3f58c05bc2d34de3aa7e956dc8c9.png?imageMogr2/format/webp",
"pid": "600099656", "pid": "600099656",
"sid": "2012514003", "sid": "2012514003",
@@ -485,7 +485,7 @@
{ {
"id": 32, "id": 32,
"videoIndex": 0, "videoIndex": 0,
"channel": "央视频道", "channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/54a6863656fdfd8f803be193ddf22441c5000a108833889816fd2d8911715ce8.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/54a6863656fdfd8f803be193ddf22441c5000a108833889816fd2d8911715ce8.png?imageMogr2/format/webp",
"pid": "600099651", "pid": "600099651",
"sid": "2012513503", "sid": "2012513503",
@@ -500,7 +500,7 @@
{ {
"id": 33, "id": 33,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/9bd372ca292a82ce3aa08772b07efc4af1f85c21d1f268ea33440c49e9a0a488.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/9bd372ca292a82ce3aa08772b07efc4af1f85c21d1f268ea33440c49e9a0a488.png?imageMogr2/format/webp",
"pid": "600002483", "pid": "600002483",
"sid": "2000292403", "sid": "2000292403",
@@ -515,7 +515,7 @@
{ {
"id": 34, "id": 34,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/4120e89d3079d08aa17d382f69a2308ec70839b278367763c34a34666c75cb88.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/4120e89d3079d08aa17d382f69a2308ec70839b278367763c34a34666c75cb88.png?imageMogr2/format/webp",
"pid": "600002475", "pid": "600002475",
"sid": "2000296203", "sid": "2000296203",
@@ -531,7 +531,7 @@
{ {
"id": 35, "id": 35,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/7a6be5a2bb1dc53a945c016ff1f525dc4a84c51db371c15c89aa55404b0ba784.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/7a6be5a2bb1dc53a945c016ff1f525dc4a84c51db371c15c89aa55404b0ba784.png?imageMogr2/format/webp",
"pid": "600002508", "pid": "600002508",
"sid": "2000294503", "sid": "2000294503",
@@ -547,7 +547,7 @@
{ {
"id": 36, "id": 36,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/ac4ed6058a87c101ae7147ebc38905d0cae047fb73fd277ee5049b84f52bda36.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/ac4ed6058a87c101ae7147ebc38905d0cae047fb73fd277ee5049b84f52bda36.png?imageMogr2/format/webp",
"pid": "600002505", "pid": "600002505",
"sid": "2000281303", "sid": "2000281303",
@@ -563,7 +563,7 @@
{ {
"id": 37, "id": 37,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/380ad685c0c1d5b2c902246b8d2df6d3f9b45e2837abcfe493075bbded597a31.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/380ad685c0c1d5b2c902246b8d2df6d3f9b45e2837abcfe493075bbded597a31.png?imageMogr2/format/webp",
"pid": "600002521", "pid": "600002521",
"sid": "2000295603", "sid": "2000295603",
@@ -579,7 +579,7 @@
{ {
"id": 38, "id": 38,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/3c760d0d00463855890e8a1864ea4a6b6dd66b90c29b4ac714a4b17c16519871.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/3c760d0d00463855890e8a1864ea4a6b6dd66b90c29b4ac714a4b17c16519871.png?imageMogr2/format/webp",
"pid": "600002503", "pid": "600002503",
"sid": "2000294103", "sid": "2000294103",
@@ -595,7 +595,7 @@
{ {
"id": 39, "id": 39,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/22d403f07a7cf5410b3ad3ddb65a11aa229a32475fac213f5344c9f0ec330ca1.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/22d403f07a7cf5410b3ad3ddb65a11aa229a32475fac213f5344c9f0ec330ca1.png?imageMogr2/format/webp",
"pid": "600002513", "pid": "600002513",
"sid": "2000294803", "sid": "2000294803",
@@ -610,7 +610,7 @@
{ {
"id": 40, "id": 40,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/28886880a4dc0f06fb7e0a528a1def0591d61a65870e29176ede0cc92033bbfd.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/28886880a4dc0f06fb7e0a528a1def0591d61a65870e29176ede0cc92033bbfd.png?imageMogr2/format/webp",
"pid": "600002485", "pid": "600002485",
"sid": "2000292703", "sid": "2000292703",
@@ -625,7 +625,7 @@
{ {
"id": 41, "id": 41,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/54b7e97cb816bb223fe05f3fc44da2c7820eb66e8550c19d23100f2c414ecc38.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/54b7e97cb816bb223fe05f3fc44da2c7820eb66e8550c19d23100f2c414ecc38.png?imageMogr2/format/webp",
"pid": "600002509", "pid": "600002509",
"sid": "2000294203", "sid": "2000294203",
@@ -641,7 +641,7 @@
{ {
"id": 42, "id": 42,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/657651f411de2673d1770d9a78b44c1265704f7468cc41d4be7f51d630768494.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/657651f411de2673d1770d9a78b44c1265704f7468cc41d4be7f51d630768494.png?imageMogr2/format/webp",
"pid": "600002531", "pid": "600002531",
"sid": "2000297803", "sid": "2000297803",
@@ -657,7 +657,7 @@
{ {
"id": 43, "id": 43,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/74925962148a6d31c85808b6cd4e444c2a54bab393d2c5fc85e960b50e22fa86.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/74925962148a6d31c85808b6cd4e444c2a54bab393d2c5fc85e960b50e22fa86.png?imageMogr2/format/webp",
"pid": "600002525", "pid": "600002525",
"sid": "2000296103", "sid": "2000296103",
@@ -673,7 +673,7 @@
{ {
"id": 44, "id": 44,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/d545becdc81c60197b08c7f47380705e4665ed3fe55efc8b855e486f6e655378.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/d545becdc81c60197b08c7f47380705e4665ed3fe55efc8b855e486f6e655378.png?imageMogr2/format/webp",
"pid": "600002493", "pid": "600002493",
"sid": "2000293403", "sid": "2000293403",
@@ -689,7 +689,7 @@
{ {
"id": 45, "id": 45,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/4eb45f4781d33d872af027dc01c941559aab55667dd99cc5c22bef7037807b13.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/4eb45f4781d33d872af027dc01c941559aab55667dd99cc5c22bef7037807b13.png?imageMogr2/format/webp",
"pid": "600002490", "pid": "600002490",
"sid": "2000293303", "sid": "2000293303",
@@ -705,7 +705,7 @@
{ {
"id": 46, "id": 46,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/f4f23633c578beea49a3841d88d3490100f029ee349059fa532869db889872c5.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/f4f23633c578beea49a3841d88d3490100f029ee349059fa532869db889872c5.png?imageMogr2/format/webp",
"pid": "600002309", "pid": "600002309",
"sid": "2000272103", "sid": "2000272103",
@@ -721,7 +721,7 @@
{ {
"id": 47, "id": 47,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/d8273ae9be698ce2db21f5b886ecac95a73429593f93713c60ed8c12c38bf0d3.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/d8273ae9be698ce2db21f5b886ecac95a73429593f93713c60ed8c12c38bf0d3.png?imageMogr2/format/webp",
"pid": "600002498", "pid": "600002498",
"sid": "2000293903", "sid": "2000293903",
@@ -737,7 +737,7 @@
{ {
"id": 48, "id": 48,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/a66c836bd98ba3e41a2e9a570d4b9c50dedc6839e9de333e2e78212ad505f37e.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/a66c836bd98ba3e41a2e9a570d4b9c50dedc6839e9de333e2e78212ad505f37e.png?imageMogr2/format/webp",
"pid": "600002520", "pid": "600002520",
"sid": "2000295503", "sid": "2000295503",
@@ -753,7 +753,7 @@
{ {
"id": 49, "id": 49,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/f35fa04b51b1ee4984b03578b65403570868ebca03c6c01e11b097f999a58d9b.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/f35fa04b51b1ee4984b03578b65403570868ebca03c6c01e11b097f999a58d9b.png?imageMogr2/format/webp",
"pid": "600002532", "pid": "600002532",
"sid": "2000298003", "sid": "2000298003",
@@ -769,7 +769,7 @@
{ {
"id": 50, "id": 50,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/d59fec04c902e3581c617136d02d4b9b8c4cbe64272781ddd3525e80c823edb7.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/d59fec04c902e3581c617136d02d4b9b8c4cbe64272781ddd3525e80c823edb7.png?imageMogr2/format/webp",
"pid": "600002481", "pid": "600002481",
"sid": "2000292203", "sid": "2000292203",
@@ -785,7 +785,7 @@
{ {
"id": 51, "id": 51,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/3276a414ae0eaa0f116f2045cd913367967d0c7c1e978e8621ac3879436c6ed7.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/3276a414ae0eaa0f116f2045cd913367967d0c7c1e978e8621ac3879436c6ed7.png?imageMogr2/format/webp",
"pid": "600002516", "pid": "600002516",
"sid": "2000295003", "sid": "2000295003",
@@ -801,7 +801,7 @@
{ {
"id": 52, "id": 52,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/3208fe6564a293c21b711333fb3edb05bb5b406cff840573c9a8d839680a1579.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/3208fe6564a293c21b711333fb3edb05bb5b406cff840573c9a8d839680a1579.png?imageMogr2/format/webp",
"pid": "600002484", "pid": "600002484",
"sid": "2000292503", "sid": "2000292503",
@@ -817,7 +817,7 @@
{ {
"id": 53, "id": 53,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/6e060391fde0469801fc3d84dbf204b4f8d650d251f17d7595a6964c0bb99e81.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/6e060391fde0469801fc3d84dbf204b4f8d650d251f17d7595a6964c0bb99e81.png?imageMogr2/format/webp",
"pid": "600002506", "pid": "600002506",
"sid": "2000291503", "sid": "2000291503",
@@ -833,7 +833,7 @@
{ {
"id": 54, "id": 54,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "", "logo": "",
"pid": "600152137", "pid": "600152137",
"sid": "2019927003", "sid": "2019927003",
@@ -848,7 +848,7 @@
{ {
"id": 55, "id": 55,
"videoIndex": 0, "videoIndex": 0,
"channel": "地方频道", "channel": "地方",
"logo": "", "logo": "",
"pid": "600152138", "pid": "600152138",
"sid": "2019927403", "sid": "2019927403",
@@ -863,7 +863,7 @@
{ {
"id": 56, "id": 56,
"videoIndex": 0, "videoIndex": 0,
"channel": "国际频道", "channel": "国际",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/a72dff758ca1c17cd0ecc8cedc11b893d208f409d5e6302faa0e9d298848abc3.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/a72dff758ca1c17cd0ecc8cedc11b893d208f409d5e6302faa0e9d298848abc3.png?imageMogr2/format/webp",
"pid": "600014550", "pid": "600014550",
"sid": "2001656803", "sid": "2001656803",
@@ -878,7 +878,7 @@
{ {
"id": 57, "id": 57,
"videoIndex": 0, "videoIndex": 0,
"channel": "国际频道", "channel": "国际",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/a8d0046a47433d952bf6ed17062deb8bd2184ba9aec0f7781df6bf9487a3ffcf.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/a8d0046a47433d952bf6ed17062deb8bd2184ba9aec0f7781df6bf9487a3ffcf.png?imageMogr2/format/webp",
"pid": "600084704", "pid": "600084704",
"sid": "2010153503", "sid": "2010153503",
@@ -893,7 +893,7 @@
{ {
"id": 58, "id": 58,
"videoIndex": 0, "videoIndex": 0,
"channel": "国际频道", "channel": "国际",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/bf0a820893cbaf20dd0333e27042e1ef9c8806e5b602b6a8c95af399db0bc77a.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/bf0a820893cbaf20dd0333e27042e1ef9c8806e5b602b6a8c95af399db0bc77a.png?imageMogr2/format/webp",
"pid": "600084758", "pid": "600084758",
"sid": "2010152603", "sid": "2010152603",
@@ -908,7 +908,7 @@
{ {
"id": 59, "id": 59,
"videoIndex": 0, "videoIndex": 0,
"channel": "国际频道", "channel": "国际",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/2e44e2aa3e7a1cedf07fd0ae59fe69e86a60a2632660a006e3e9e7397b2d107e.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202306/2e44e2aa3e7a1cedf07fd0ae59fe69e86a60a2632660a006e3e9e7397b2d107e.png?imageMogr2/format/webp",
"pid": "600084782", "pid": "600084782",
"sid": "2010155203", "sid": "2010155203",
@@ -923,7 +923,7 @@
{ {
"id": 60, "id": 60,
"videoIndex": 0, "videoIndex": 0,
"channel": "国际频道", "channel": "国际",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202309/7c337e3dbe64402ec7e4678a619a4a6d95144e42f35161181ff78e143b7cf67a.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202309/7c337e3dbe64402ec7e4678a619a4a6d95144e42f35161181ff78e143b7cf67a.png?imageMogr2/format/webp",
"pid": "600084744", "pid": "600084744",
"sid": "2010152503", "sid": "2010152503",
@@ -939,7 +939,7 @@
{ {
"id": 61, "id": 61,
"videoIndex": 0, "videoIndex": 0,
"channel": "国际频道", "channel": "国际",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202309/74d3ac436a7e374879578de1d87a941fbf566d39d5632b027c5097891ed32bd5.png?imageMogr2/format/webp", "logo": "https://resources.yangshipin.cn/assets/oms/image/202309/74d3ac436a7e374879578de1d87a941fbf566d39d5632b027c5097891ed32bd5.png?imageMogr2/format/webp",
"pid": "600084781", "pid": "600084781",
"sid": "2010155403", "sid": "2010155403",
+1
View File
@@ -2,4 +2,5 @@
<color name="fastlane_background">#30000000</color> <color name="fastlane_background">#30000000</color>
<color name="black">#000</color> <color name="black">#000</color>
<color name="white">#FFF</color> <color name="white">#FFF</color>
<color name="gray_overlay">#7F000000</color>
</resources> </resources>
+1
View File
@@ -3,4 +3,5 @@
<string name="title_channel_reversal">换台反转</string> <string name="title_channel_reversal">换台反转</string>
<string name="title_channel_num">数字选台</string> <string name="title_channel_num">数字选台</string>
<string name="check_version">检查更新</string> <string name="check_version">检查更新</string>
<string name="title_boot_startup">开机自启</string>
</resources> </resources>
+2 -2
View File
@@ -10,8 +10,8 @@
</style> </style>
<style name="CustomImageCardViewImageStyle" parent="Widget.Leanback.ImageCardView.ImageStyle"> <style name="CustomImageCardViewImageStyle" parent="Widget.Leanback.ImageCardView.ImageStyle">
<item name="android:layout_width">143dp</item> <item name="android:layout_width">160dp</item>
<item name="android:layout_height">50dp</item> <item name="android:layout_height">40dp</item>
<item name="android:adjustViewBounds">true</item> <item name="android:adjustViewBounds">true</item>
<item name="android:contentDescription">@null</item> <item name="android:contentDescription">@null</item>
<item name="android:scaleType">centerCrop</item> <item name="android:scaleType">centerCrop</item>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB