Compare commits

..
36 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
Li ZongYing 353dc0570f fix ysp 2024-02-09 22:36:48 +08:00
Li ZongYing d7a60531ab fix cctv6 2024-02-08 00:22:24 +08:00
Li ZongYing 764f2a1549 fix time error 2024-02-07 19:21:17 +08:00
Li ZongYing aa4d52681f fix ysp 2024-02-07 03:05:11 +08:00
Li ZongYing 8137515605 fix ysp err 2024-02-05 06:25:31 +08:00
Li ZongYing 6c16408b90 upgrade player 2024-02-04 22:23:28 +08:00
Li ZongYing 44ab3a6669 fix channel number did not disappear 2024-02-03 23:30:37 +08:00
Li ZongYing 917b245bbe readme 2024-02-02 23:34:39 +08:00
Li ZongYing dd4550cf4f replay on resume 2024-02-01 18:10:58 +08:00
Li ZongYing 4bdc625718 add channel 2024-01-26 18:30:31 +08:00
Li ZongYing cda19995b2 optimize channel switch 2024-01-25 15:12:47 +08:00
Li ZongYing 1eb2f257b5 change channel order 2024-01-25 11:04:06 +08:00
Li ZongYing 9feb0dc4bc channel num setting 2024-01-19 12:18:17 +08:00
Li ZongYing 46311036d6 number selection 2024-01-17 16:27:25 +08:00
Li ZongYing 1aade3a48d 17ok 2024-01-14 18:58:37 +08:00
Li ZongYing 2528cf33db test 17 2024-01-14 10:09:59 +08:00
Li ZongYing 4364f66ec5 17 2024-01-14 02:53:09 +08:00
Li ZongYing 49cd7b3af2 help page 2024-01-11 16:32:08 +08:00
33 changed files with 1995 additions and 1846 deletions
+15 -1
View File
@@ -14,6 +14,17 @@
## 更新日志 ## 更新日志
### v1.6.6(通用)
* 更新重庆卫视图标
* 凤凰卫视增强画质
* 凤凰卫视增加EPG
### v1.6.5(安卓5及以上专用)
* 增加CETV1图标
* 稳定性提升
### v1.6.4(通用) ### v1.6.4(通用)
* 增加CETV1 * 增加CETV1
@@ -214,14 +225,17 @@ adb install my-tv.apk
* 音量不同 * 音量不同
* 大湾区卫视、广东4k超高清、广东珠江、三沙卫视 * 大湾区卫视、广东4k超高清、广东珠江、三沙卫视
* CETV教育频道
* CHC高清三个电影频道 * CHC高清三个电影频道
* 地方频道 * 地方频道
* 收藏夹 * 收藏夹
* 自定义源 * 自定义源
* 海外 * 海外
* 1.5.0 无法安装,1.5.1 可以安装
* 节目增加预告 * 节目增加预告
* 频道列表优化 * 频道列表优化
* 自动更新 * epg更新后菜单更新
* 防止菜单第一个获取焦点
## 赞赏 ## 赞赏
+1 -1
View File
@@ -101,7 +101,7 @@ dependencies {
implementation "androidx.media3:media3-exoplayer-hls:$media3_version" implementation "androidx.media3:media3-exoplayer-hls:$media3_version"
// 21:2.9.0 17:2.6.4 // 21:2.9.0 17:2.6.4
def retrofit2_version = "2.9.0" def retrofit2_version = "2.6.4"
implementation 'com.google.protobuf:protobuf-kotlin:3.25.1' implementation 'com.google.protobuf:protobuf-kotlin:3.25.1'
implementation "com.squareup.retrofit2:converter-gson:$retrofit2_version" implementation "com.squareup.retrofit2:converter-gson:$retrofit2_version"
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,139 @@
package com.lizongying.mytv
import android.graphics.Color
import android.view.ContextThemeWrapper
import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
import android.view.animation.ScaleAnimation
import android.widget.ImageView
import androidx.leanback.widget.ImageCardView
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.lizongying.mytv.models.TVListViewModel
import com.lizongying.mytv.models.TVViewModel
class CardAdapter(
private val recyclerView: RecyclerView,
private val mainFragment: MainFragment,
private var tvListViewModel: TVListViewModel,
) :
RecyclerView.Adapter<CardAdapter.ViewHolder>() {
private var listener: ItemListener? = null
private var focused: View? = null
var visiable = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val cardView = object :
ImageCardView(ContextThemeWrapper(parent.context, R.style.CustomImageCardTheme)) {}
cardView.isFocusable = true
cardView.isFocusableInTouchMode = true
return ViewHolder(cardView)
}
fun clear() {
focused?.clearFocus()
recyclerView.invalidate()
}
private fun startScaleAnimation(view: View, fromScale: Float, toScale: Float, duration: Long) {
val scaleAnimation = ScaleAnimation(
fromScale, toScale,
fromScale, toScale,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f
)
scaleAnimation.duration = duration
scaleAnimation.fillAfter = false
view.startAnimation(scaleAnimation)
}
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
val item = tvListViewModel.getTVViewModel(position)
val tvViewModel = item as TVViewModel
val cardView = viewHolder.view as ImageCardView
cardView.tag = tvViewModel
val onFocusChangeListener = View.OnFocusChangeListener { view, hasFocus ->
if (hasFocus) {
listener?.onItemHasFocus(item)
focused = cardView
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 { _ ->
listener?.onItemClicked(item)
}
cardView.setOnKeyListener { _, keyCode, event: KeyEvent? ->
if (event?.action == KeyEvent.ACTION_DOWN) {
return@setOnKeyListener listener?.onKey(keyCode) ?: false
}
false
}
cardView.titleText = tvViewModel.title.value
Glide.with(viewHolder.view.context)
.load(tvViewModel.logo.value)
.centerInside()
.into(cardView.mainImageView)
cardView.mainImageView.setBackgroundColor(Color.WHITE)
cardView.setMainImageScaleType(ImageView.ScaleType.CENTER_INSIDE)
val epg = tvViewModel.epg.value?.filter { it.beginTime < Utils.getDateTimestamp() }
if (!epg.isNullOrEmpty()) {
cardView.contentText = epg.last().title
} else {
cardView.contentText = ""
}
}
override fun getItemCount() = tvListViewModel.size()
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val view = itemView
}
fun toPosition(position: Int) {
recyclerView.post {
recyclerView.scrollToPosition(position)
recyclerView.getChildAt(position)?.isSelected
recyclerView.getChildAt(position)?.requestFocus()
}
}
interface ItemListener {
fun onItemHasFocus(tvViewModel: TVViewModel)
fun onItemClicked(tvViewModel: TVViewModel)
fun onKey(keyCode: Int): Boolean
}
fun setItemListener(listener: ItemListener) {
this.listener = listener
}
companion object {
private const val TAG = "CardAdapter"
}
}
@@ -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
}
}
@@ -1,157 +0,0 @@
package com.lizongying.mytv
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
/**
*@author LeGend
*@date 2024/2/4 22:42
*/
object ChannelUtils {
/**
* 获取服务器channel版本
*
* @param context Context
*
* @return 服务器channel版本
*/
suspend fun getServerVersion(context: Context): Int {
return withContext(Dispatchers.IO) {
val client = okhttp3.OkHttpClient.Builder().connectTimeout(500, java.util.concurrent.TimeUnit.MILLISECONDS)
.readTimeout(1, java.util.concurrent.TimeUnit.SECONDS).build()
client.newCall(okhttp3.Request.Builder().url(getServerVersionUrl(context)).build()).execute()
.use { response ->
if (!response.isSuccessful) throw java.io.IOException("Unexpected code $response")
val body = response.body()
body?.string()?.toInt() ?: 0
}
}
}
/**
* 获取服务器channel
*
* @param url String 服务器地址
*
* @return Array<TV> 服务器channel
*
* @throws java.io.IOException 网络请求失败
*/
suspend fun getServerChannel(url: String): List<TV> {
val result = withContext(Dispatchers.IO) {
val client = okhttp3.OkHttpClient.Builder().connectTimeout(500, java.util.concurrent.TimeUnit.MILLISECONDS)
.readTimeout(1, java.util.concurrent.TimeUnit.SECONDS).build()
val request = okhttp3.Request.Builder().url(url).build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw java.io.IOException("Unexpected code $response")
val body = response.body()
body?.string() ?: ""
}
}
return withContext(Dispatchers.Default) {
val type = object : com.google.gson.reflect.TypeToken<List<TV>>() {}.type
com.google.gson.Gson().fromJson(result, type)
}
}
/**
* 获取服务器地址
*
* @param context Context
*
* @return 服务器地址
*/
fun getServerUrl(context: Context): String {
return context.resources.getString(R.string.server_url)
}
/**
* 获取serverVersion的URL
*
* @param context Context
*
* @return serverVersionURL 服务器版本地址
*/
suspend fun getServerVersionUrl(context: Context): String {
return withContext(Dispatchers.IO) {
context.resources.getString(R.string.server_version_url)
}
}
/**
* 获取本地channel版本
*
* @param context Context
*
* @return 本地channel
*/
suspend fun getLocalVersion(context: Context): Int {
return withContext(Dispatchers.IO) {
val file = File(getAppDirectory(context), "channels")
//检查本地是否已经有保存的channels.json,若无保存的Channel.json则从读取assert中文件
val savedVersion =
context.getSharedPreferences("saved_version", Context.MODE_PRIVATE).getInt("version", Integer.MIN_VALUE)
if (!file.exists() || savedVersion == Integer.MIN_VALUE) {
context.resources.getInteger(R.integer.local_channel_version)
} else {
savedVersion
}
}
}
/**
* 获取本地可读取的目录
* @param context Context
*
* @return 可读取的目录
*/
private fun getAppDirectory(context: Context): File {
return context.filesDir
}
/**
* 获取本地channel
*
* @param context Context
*
* @return Array<TV> 本地channel
*/
suspend fun getLocalChannel(context: Context): List<TV> {
val str = withContext(Dispatchers.IO) {
if (File(getAppDirectory(context), "channels").exists()) {
File(getAppDirectory(context), "channels").readText()
} else {
context.resources.openRawResource(R.raw.channels).bufferedReader().use { it.readText() }
}
}
return withContext(Dispatchers.Default) {
val type = object : com.google.gson.reflect.TypeToken<List<TV>>() {}.type
com.google.gson.Gson().fromJson(str, type)
}
}
/**
* 更新channels.json
*
* @param context Context
*
* @return 无
*
* @throws java.io.IOException 写入失败
*/
suspend fun updateLocalChannel(context: Context, version: Int, channels: List<TV>) {
withContext(Dispatchers.IO) {
val file = File(getAppDirectory(context), "channels")
if (!file.exists()) {
file.createNewFile()
}
file.writeText(com.google.gson.Gson().toJson(channels))
context.getSharedPreferences("saved_version", Context.MODE_PRIVATE).edit().putInt(
"version", version
).apply()
}
}
}
@@ -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,35 +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)
"CETV1" -> Glide.with(this)
.load(R.drawable.cetv1)
.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)
@@ -1,10 +1,5 @@
package com.lizongying.mytv package com.lizongying.mytv
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
@@ -16,23 +11,17 @@ 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(), Request.RequestListener { 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
@@ -42,16 +31,6 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
private val delayHideMain: Long = 10000 private val delayHideMain: Long = 10000
private val delayHideSetting: Long = 10000 private val delayHideSetting: Long = 10000
// 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)
@@ -74,20 +53,6 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
.commit() .commit()
} }
gestureDetector = GestureDetector(this, GestureListener()) gestureDetector = GestureDetector(this, GestureListener())
val packageInfo = getPackageInfo()
val versionName = packageInfo.versionName
val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode
} else {
packageInfo.versionCode.toLong()
}
settingFragment = SettingFragment(
versionName,
versionCode,
SP.channelReversal,
SP.channelNum,
SP.bootStartup
)
} }
fun showInfoFragment(tvViewModel: TVViewModel) { fun showInfoFragment(tvViewModel: TVViewModel) {
@@ -140,6 +105,7 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
val transaction = supportFragmentManager.beginTransaction() val transaction = supportFragmentManager.beginTransaction()
if (mainFragment.isHidden) { if (mainFragment.isHidden) {
mainFragment.setPosition()
transaction.show(mainFragment) transaction.show(mainFragment)
mainActive() mainActive()
} else { } else {
@@ -306,7 +272,9 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
}, 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")
@@ -418,6 +386,7 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
} }
KeyEvent.KEYCODE_DPAD_LEFT -> { KeyEvent.KEYCODE_DPAD_LEFT -> {
channelUp()
// if (mainFragment.isHidden) { // if (mainFragment.isHidden) {
// prevSource() // prevSource()
// } else { // } else {
@@ -431,6 +400,7 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
} }
KeyEvent.KEYCODE_DPAD_RIGHT -> { KeyEvent.KEYCODE_DPAD_RIGHT -> {
channelDown()
// if (mainFragment.isHidden) { // if (mainFragment.isHidden) {
// nextSource() // nextSource()
// } else { // } else {
@@ -442,60 +412,18 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
// } // }
} }
} }
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")
@@ -528,5 +456,4 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
private companion object { private companion object {
const val TAG = "MainActivity" const val TAG = "MainActivity"
} }
} }
@@ -1,51 +1,45 @@
package com.lizongying.mytv package com.lizongying.mytv
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.View
import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.leanback.app.BrowseSupportFragment import androidx.fragment.app.Fragment
import androidx.leanback.widget.ArrayObjectAdapter
import androidx.leanback.widget.HeaderItem
import androidx.leanback.widget.ListRow
import androidx.leanback.widget.ListRowPresenter
import androidx.leanback.widget.ListRowPresenter.SelectItemViewHolderTask
import androidx.leanback.widget.OnItemViewClickedListener
import androidx.leanback.widget.OnItemViewSelectedListener
import androidx.leanback.widget.Presenter
import androidx.leanback.widget.Row
import androidx.leanback.widget.RowPresenter
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.lizongying.mytv.Utils.getDateTimestamp import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.lizongying.mytv.Utils.dpToPx
import com.lizongying.mytv.api.YSP import com.lizongying.mytv.api.YSP
import com.lizongying.mytv.databinding.RowBinding
import com.lizongying.mytv.databinding.ShowBinding
import com.lizongying.mytv.models.TVListViewModel import com.lizongying.mytv.models.TVListViewModel
import com.lizongying.mytv.models.TVViewModel import com.lizongying.mytv.models.TVViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MainFragment : BrowseSupportFragment() {
class MainFragment : Fragment(), CardAdapter.ItemListener {
private var itemPosition = 0 private var itemPosition = 0
private var rowsAdapter: ArrayObjectAdapter? = null private var rowList: MutableList<View> = mutableListOf()
private var _binding: ShowBinding? = null
private val binding get() = _binding!!
var tvListViewModel = TVListViewModel() var tvListViewModel = TVListViewModel()
private var lastVideoUrl = "" private var lastVideoUrl = ""
private val handler = Handler(Looper.getMainLooper()) override fun onCreateView(
private lateinit var mUpdateProgramRunnable: UpdateProgramRunnable inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
override fun onCreate(savedInstanceState: Bundle?) { ): View {
Log.i(TAG, "onCreate") _binding = ShowBinding.inflate(inflater, container, false)
super.onCreate(savedInstanceState) return binding.root
headersState = HEADERS_DISABLED
}
override fun onStart() {
Log.i(TAG, "onStart")
super.onStart()
} }
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
@@ -53,172 +47,185 @@ class MainFragment : BrowseSupportFragment() {
activity?.let { YSP.init(it) } activity?.let { YSP.init(it) }
loadRows()
setupEventListeners()
mUpdateProgramRunnable = UpdateProgramRunnable()
handler.post(mUpdateProgramRunnable)
tvListViewModel.tvListViewModel.value?.forEach { tvViewModel ->
tvViewModel.errInfo.observe(viewLifecycleOwner) { _ ->
if (tvViewModel.errInfo.value != null
&& tvViewModel.id.value == itemPosition
) {
Toast.makeText(context, tvViewModel.errInfo.value, Toast.LENGTH_SHORT).show()
}
}
tvViewModel.ready.observe(viewLifecycleOwner) { _ ->
// not first time && channel not change
if (tvViewModel.ready.value != null
&& tvViewModel.id.value == itemPosition
&& check(tvViewModel)
) {
Log.i(TAG, "ready ${tvViewModel.title.value}")
(activity as? MainActivity)?.play(tvViewModel)
}
}
tvViewModel.change.observe(viewLifecycleOwner) { _ ->
if (tvViewModel.change.value != null) {
val title = tvViewModel.title.value
Log.i(TAG, "switch $title")
if (tvViewModel.pid.value != "") {
Log.i(TAG, "request $title")
lifecycleScope.launch(Dispatchers.IO) {
tvViewModel.let { Request.fetchData(it) }
}
(activity as? MainActivity)?.showInfoFragment(tvViewModel)
setSelectedPosition(
tvViewModel.getRowPosition(), true,
SelectItemViewHolderTask(tvViewModel.getItemPosition())
)
} else {
if (check(tvViewModel)) {
(activity as? MainActivity)?.play(tvViewModel)
(activity as? MainActivity)?.showInfoFragment(tvViewModel)
setSelectedPosition(
tvViewModel.getRowPosition(), true,
SelectItemViewHolderTask(tvViewModel.getItemPosition())
)
}
}
}
}
}
(activity as MainActivity).fragmentReady()
}
fun toLastPosition() {
setSelectedPosition(
selectedPosition, false,
SelectItemViewHolderTask(tvListViewModel.maxNum[selectedPosition] - 1)
)
}
fun toFirstPosition() {
setSelectedPosition(
selectedPosition, false,
SelectItemViewHolderTask(0)
)
}
override fun startHeadersTransition(withHeaders: Boolean) {
}
private fun loadRows() {
rowsAdapter = ArrayObjectAdapter(ListRowPresenter())
val cardPresenter = CardPresenter(viewLifecycleOwner)
var idx: Long = 0
for ((k, v) in TVList.list) {
val listRowAdapter = ArrayObjectAdapter(cardPresenter)
for ((idx2, v1) in v.withIndex()) {
val tvViewModel = TVViewModel(v1)
tvViewModel.setRowPosition(idx.toInt())
tvViewModel.setItemPosition(idx2)
tvListViewModel.addTVViewModel(tvViewModel)
listRowAdapter.add(tvViewModel)
}
tvListViewModel.maxNum.add(v.size)
val header = HeaderItem(idx, k)
rowsAdapter!!.add(ListRow(header, listRowAdapter))
idx++
}
adapter = rowsAdapter
itemPosition = SP.itemPosition itemPosition = SP.itemPosition
if (itemPosition >= tvListViewModel.size()) {
itemPosition = 0
}
tvListViewModel.setItemPosition(itemPosition)
}
fun prevSource() {
view?.post { view?.post {
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition) val content = binding.content
if (tvViewModel != null) {
if (tvViewModel.videoUrl.value!!.size > 1) { var idx: Long = 0
val videoIndex = tvViewModel.videoIndex.value?.minus(1) for ((k, v) in TVList.list) {
if (videoIndex == -1) { val itemBinding: RowBinding =
tvViewModel.setVideoIndex(tvViewModel.videoUrl.value!!.size - 1) RowBinding.inflate(layoutInflater, content, false)
val tvListViewModelCurrent = TVListViewModel()
for ((idx2, v1) in v.withIndex()) {
val tvViewModel = TVViewModel(v1)
tvViewModel.setRowPosition(idx.toInt())
tvViewModel.setItemPosition(idx2)
tvListViewModelCurrent.addTVViewModel(tvViewModel)
tvListViewModel.addTVViewModel(tvViewModel)
updateEPG(tvViewModel)
}
tvListViewModel.maxNum.add(v.size)
val adapter =
CardAdapter(
itemBinding.rowItems,
this,
tvListViewModelCurrent,
)
rowList.add(itemBinding.rowItems)
adapter.setItemListener(this)
itemBinding.rowHeader.text = k
itemBinding.rowItems.tag = idx.toInt()
itemBinding.rowItems.adapter = adapter
itemBinding.rowItems.layoutManager =
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()
} }
tvViewModel.changed() })
}
}
}
}
fun nextSource() { val itemDecoration = context?.let { GrayOverlayItemDecoration(it) }
view?.post { if (itemDecoration != null) {
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition) itemBinding.rowItems.addItemDecoration(itemDecoration)
if (tvViewModel != null) { }
if (tvViewModel.videoUrl.value!!.size > 1) {
val videoIndex = tvViewModel.videoIndex.value?.plus(1) val layoutParams = itemBinding.row.layoutParams as ViewGroup.MarginLayoutParams
if (videoIndex == tvViewModel.videoUrl.value!!.size) { layoutParams.topMargin = dpToPx(11F)
tvViewModel.setVideoIndex(0) itemBinding.row.layoutParams = layoutParams
content.addView(itemBinding.row)
idx++
}
if (itemPosition >= tvListViewModel.size()) {
itemPosition = 0
}
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.tvListViewModel.value?.forEach { tvViewModel ->
tvViewModel.errInfo.observe(viewLifecycleOwner) { _ ->
if (tvViewModel.errInfo.value != null
&& tvViewModel.id.value == itemPosition
) {
Toast.makeText(context, tvViewModel.errInfo.value, Toast.LENGTH_SHORT)
.show()
}
}
tvViewModel.ready.observe(viewLifecycleOwner) { _ ->
// not first time && channel not change
if (tvViewModel.ready.value != null
&& tvViewModel.id.value == itemPosition
&& check(tvViewModel)
) {
Log.i(TAG, "ready ${tvViewModel.title.value}")
(activity as? MainActivity)?.play(tvViewModel)
}
}
tvViewModel.change.observe(viewLifecycleOwner) { _ ->
if (tvViewModel.change.value != null) {
val title = tvViewModel.title.value
Log.i(TAG, "switch $title")
if (tvViewModel.pid.value != "") {
Log.i(TAG, "request $title")
lifecycleScope.launch(Dispatchers.IO) {
tvViewModel.let { Request.fetchData(it) }
}
(activity as? MainActivity)?.showInfoFragment(tvViewModel)
setPosition(
tvViewModel.getRowPosition(), tvViewModel.getItemPosition()
)
} else {
if (check(tvViewModel)) {
// TODO lastVideoUrl
(activity as? MainActivity)?.play(tvViewModel)
(activity as? MainActivity)?.showInfoFragment(tvViewModel)
setPosition(
tvViewModel.getRowPosition(), tvViewModel.getItemPosition()
)
}
}
} }
tvViewModel.changed()
} }
} }
(activity as MainActivity).fragmentReady()
} }
} }
private fun setupEventListeners() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
onItemViewClickedListener = ItemViewClickedListener() super.onViewCreated(view, savedInstanceState)
onItemViewSelectedListener = ItemViewSelectedListener()
} }
private inner class ItemViewClickedListener : OnItemViewClickedListener { override fun onKey(keyCode: Int): Boolean {
override fun onItemClicked( if (this.isHidden) {
itemViewHolder: Presenter.ViewHolder, when (keyCode) {
item: Any, KeyEvent.KEYCODE_DPAD_UP -> {
rowViewHolder: RowPresenter.ViewHolder, (activity as MainActivity).onKey(keyCode)
row: Row return true
) { }
if (item is TVViewModel) {
if (itemPosition != item.id.value!!) { KeyEvent.KEYCODE_DPAD_DOWN -> {
itemPosition = item.id.value!! (activity as MainActivity).onKey(keyCode)
tvListViewModel.setItemPosition(itemPosition) return true
tvListViewModel.getTVViewModel(itemPosition)?.changed()
} }
(activity as? MainActivity)?.switchMainFragment()
} }
} }
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) {
Log.i(TAG, "onItemClicked")
if (this.isHidden) {
(activity as? MainActivity)?.switchMainFragment()
return
}
if (itemPosition != tvViewModel.id.value!!) {
itemPosition = tvViewModel.id.value!!
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVViewModel(itemPosition)?.changed()
}
(activity as? MainActivity)?.switchMainFragment()
}
fun setPosition() {
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
rowList[tvViewModel!!.getRowPosition()].post {
((rowList[tvViewModel.getRowPosition()] as RecyclerView).layoutManager as GridLayoutManager).findViewByPosition(
tvViewModel.getItemPosition()
)?.requestFocus()
}
} }
private inner class ItemViewSelectedListener : OnItemViewSelectedListener { fun setPosition(rowPosition: Int, itemPosition: Int) {
override fun onItemSelected( rowList[rowPosition].post {
itemViewHolder: Presenter.ViewHolder?, item: Any?, ((rowList[rowPosition] as RecyclerView).layoutManager as GridLayoutManager).findViewByPosition(
rowViewHolder: RowPresenter.ViewHolder, row: Row itemPosition
) { )?.requestFocus()
if (item is TVViewModel) {
tvListViewModel.setItemPositionCurrent(item.id.value!!)
(activity as MainActivity).mainActive()
}
} }
} }
@@ -277,30 +284,37 @@ class MainFragment : BrowseSupportFragment() {
} }
} }
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
// }
}
} }
} }
@@ -313,18 +327,22 @@ class MainFragment : BrowseSupportFragment() {
Log.i(TAG, "onStop") Log.i(TAG, "onStop")
super.onStop() super.onStop()
SP.itemPosition = itemPosition SP.itemPosition = itemPosition
Log.i(TAG, "position saved") Log.i(TAG, "$POSITION $itemPosition 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() {
Log.i(TAG, "onDestroyView")
super.onDestroyView()
_binding = null
} }
companion object { companion object {
private const val TAG = "MainFragment" private const val TAG = "MainFragment"
private const val POSITION = "position"
} }
} }
+179 -112
View File
@@ -10,6 +10,7 @@ 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.FAuth
import com.lizongying.mytv.api.FAuthService 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.InfoV2
import com.lizongying.mytv.api.LiveInfo import com.lizongying.mytv.api.LiveInfo
@@ -55,6 +56,9 @@ object Request {
private var listener: RequestListener? = null private var listener: RequestListener? = null
private var initRetryTimes = 0
private var initRetryMaxTimes = 1
fun onCreate() { fun onCreate() {
Log.i(TAG, "onCreate") Log.i(TAG, "onCreate")
fetchInfoV2() fetchInfoV2()
@@ -65,18 +69,28 @@ object Request {
handler.removeCallbacks(tokenRunnable) handler.removeCallbacks(tokenRunnable)
} }
var call: Call<LiveInfo>? = null private var call: Call<LiveInfo>? = null
private 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 = AuthRequest(data) 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) {
@@ -91,12 +105,12 @@ object 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 {
@@ -109,12 +123,12 @@ object 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 {
@@ -129,12 +143,12 @@ object 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 {
@@ -145,8 +159,9 @@ object 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)
} }
@@ -196,13 +211,13 @@ object 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) // fetchAuth(tvModel, cookie)
} }
} else { } else {
token = "" tvModel.needGetToken = true
fetchVideo(tvModel) fetchVideo(tvModel)
// fetchAuth(tvModel) // fetchAuth(tvModel)
} }
@@ -221,13 +236,13 @@ object 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) // fetchAuth(tvModel, cookie)
} }
} else { } else {
token = "" tvModel.needGetToken = true
fetchVideo(tvModel) fetchVideo(tvModel)
// fetchAuth(tvModel) // fetchAuth(tvModel)
} }
@@ -243,13 +258,13 @@ object 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) // fetchAuth(tvModel, cookie)
} }
} else { } else {
token = "" tvModel.needGetToken = true
fetchVideo(tvModel) fetchVideo(tvModel)
// fetchAuth(tvModel) // fetchAuth(tvModel)
} }
@@ -266,12 +281,12 @@ object 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)
} }
} else { } else {
token = "" tvModel.needGetToken = true
fetchVideo(tvModel) fetchVideo(tvModel)
} }
} else { } else {
@@ -282,37 +297,31 @@ object 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 =
@@ -321,7 +330,22 @@ object 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"
@@ -329,37 +353,31 @@ object Request {
} }
} }
fun fetchVideo(tvModel: TVViewModel) { private fun fetchVideo(tvModel: TVViewModel) {
cancelCall()
Log.d(TAG, "fetchVideo") Log.d(TAG, "fetchVideo")
if (token == "") { if (tvModel.needGetToken) {
yspTokenService.getInfo("") callInfo = yspTokenService.getInfo("")
.enqueue(object : Callback<Info> { callInfo?.enqueue(object : Callback<Info> {
override fun onResponse(call: Call<Info>, response: Response<Info>) { override fun onResponse(call: Call<Info>, response: Response<Info>) {
if (response.isSuccessful && response.body()?.data?.token != null) { if (response.isSuccessful && response.body()?.data?.token != null) {
token = response.body()?.data?.token!! token = response.body()?.data?.token!!
Log.i(TAG, "info success $token") Log.i(TAG, "info success $token")
val cookie = tvModel.needGetToken = false
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token" tvModel.tokenYSPRetryTimes = 0
fetchVideo(tvModel, cookie) val cookie =
} else { "versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
Log.e(TAG, "info status error") fetchVideo(tvModel, cookie)
if (tvModel.tokenRetryTimes < tvModel.tokenRetryMaxTimes) { } else if (response.code() == 304) {
tvModel.tokenRetryTimes++ tvModel.needGetToken = false
fetchVideo(tvModel) tvModel.tokenYSPRetryTimes = 0
} else { val cookie =
if (!tvModel.getTV().mustToken) { "versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
val cookie = fetchVideo(tvModel, cookie)
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109" } else {
fetchVideo(tvModel, cookie) 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) {
@@ -369,7 +387,22 @@ object 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"
@@ -377,11 +410,8 @@ object Request {
} }
} }
private var fAuth: Call<FAuth>? = null private fun fetchFAuth(tvModel: TVViewModel) {
fun fetchFAuth(tvModel: TVViewModel) { cancelCall()
call?.cancel()
callAuth?.cancel()
fAuth?.cancel()
val title = tvModel.title.value val title = tvModel.title.value
@@ -390,19 +420,19 @@ object Request {
qa = "FHD" qa = "FHD"
} }
fAuth = fAuthService.getAuth(tokenFH, tvModel.getTV().pid, qa) callFAuth = fAuthService.getAuth(tokenFH, tvModel.getTV().pid, qa)
fAuth?.enqueue(object : Callback<FAuth> { callFAuth?.enqueue(object : Callback<FAuth> {
override fun onResponse(call: Call<FAuth>, response: Response<FAuth>) { override fun onResponse(call: Call<FAuth>, response: Response<FAuth>) {
if (response.isSuccessful && response.body()?.data?.live_url != null) { if (response.isSuccessful && response.body()?.data?.live_url != null) {
val url = response.body()?.data?.live_url!! val url = response.body()?.data?.live_url!!
// Log.d(TAG, "$title url $url") // Log.d(TAG, "$title url $url")
tvModel.addVideoUrl(url) tvModel.addVideoUrl(url)
tvModel.allReady() tvModel.allReady()
tvModel.retryTimes = 0 tvModel.tokenFHRetryTimes = 0
} else { } else {
Log.e(TAG, "auth status error") Log.e(TAG, "auth status error ${response.code()}")
if (tvModel.tokenRetryTimes < tvModel.tokenRetryMaxTimes) { if (tvModel.tokenFHRetryTimes < tvModel.tokenFHRetryMaxTimes) {
tvModel.tokenRetryTimes++ tvModel.tokenFHRetryTimes++
fetchFAuth(tvModel) fetchFAuth(tvModel)
} }
} }
@@ -410,8 +440,8 @@ object Request {
override fun onFailure(call: Call<FAuth>, t: Throwable) { override fun onFailure(call: Call<FAuth>, t: Throwable) {
Log.e(TAG, "auth request error $t") Log.e(TAG, "auth request error $t")
if (tvModel.tokenRetryTimes < tvModel.tokenRetryMaxTimes) { if (tvModel.tokenFHRetryTimes < tvModel.tokenFHRetryMaxTimes) {
tvModel.tokenRetryTimes++ tvModel.tokenFHRetryTimes++
fetchFAuth(tvModel) fetchFAuth(tvModel)
} }
} }
@@ -419,7 +449,6 @@ object Request {
} }
fun fetchData(tvModel: TVViewModel) { fun fetchData(tvModel: TVViewModel) {
Log.d(TAG, "fetchData")
if (tvModel.getTV().channel == "港澳台") { if (tvModel.getTV().channel == "港澳台") {
fetchFAuth(tvModel) fetchFAuth(tvModel)
return return
@@ -444,7 +473,7 @@ object Request {
class TokenRunnable : Runnable { class TokenRunnable : Runnable {
override fun run() { override fun run() {
fetchToken() // fetchToken()
} }
} }
@@ -463,20 +492,20 @@ object Request {
if (e != null) { if (e != null) {
handler.postDelayed( handler.postDelayed(
tokenRunnable, tokenRunnable,
max(600000, (e - 1500) * 1000).toLong() max(30 * 60 * 1000, (e - 1500) * 1000).toLong()
) )
} else { } else {
Log.e(TAG, "e empty") Log.e(TAG, "e empty")
handler.postDelayed( handler.postDelayed(
tokenRunnable, tokenRunnable,
600000 30 * 60 * 1000
) )
} }
} else { } else {
Log.e(TAG, "token empty") Log.e(TAG, "token empty")
handler.postDelayed( handler.postDelayed(
tokenRunnable, tokenRunnable,
600000 30 * 60 * 1000
) )
} }
if (!f.isNullOrEmpty()) { if (!f.isNullOrEmpty()) {
@@ -487,23 +516,35 @@ object Request {
Utils.setBetween(c * 1000L) Utils.setBetween(c * 1000L)
Log.i(TAG, "current time $c") Log.i(TAG, "current time $c")
} }
listener?.onRequestFinished()
} else { } else {
Log.e(TAG, "token status error") Log.e(TAG, "token status error")
handler.postDelayed( handler.postDelayed(
tokenRunnable, tokenRunnable,
600000 30 * 60 * 1000
) )
if (initRetryTimes < initRetryMaxTimes) {
initRetryTimes++
fetchInfoV2()
} else {
listener?.onRequestFinished()
}
} }
listener?.onRequestFinished()
} }
override fun onFailure(call: Call<InfoV2>, 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( handler.postDelayed(
tokenRunnable, tokenRunnable,
600000 30 * 60 * 1000
) )
listener?.onRequestFinished()
if (initRetryTimes < initRetryMaxTimes) {
initRetryTimes++
fetchInfoV2()
} else {
listener?.onRequestFinished()
}
} }
}) })
} }
@@ -521,27 +562,27 @@ object Request {
if (e != null) { if (e != null) {
handler.postDelayed( handler.postDelayed(
tokenRunnable, tokenRunnable,
max(600000, (e - 1500) * 1000).toLong() max(30 * 60 * 1000, (e - 1500) * 1000).toLong()
) )
} else { } else {
Log.e(TAG, "e empty") Log.e(TAG, "e empty")
handler.postDelayed( handler.postDelayed(
tokenRunnable, tokenRunnable,
600000 30 * 60 * 1000
) )
} }
} else { } else {
Log.e(TAG, "token empty") Log.e(TAG, "token empty")
handler.postDelayed( handler.postDelayed(
tokenRunnable, tokenRunnable,
600000 30 * 60 * 1000
) )
} }
} else { } else {
Log.e(TAG, "token status error") Log.e(TAG, "token status error")
handler.postDelayed( handler.postDelayed(
tokenRunnable, tokenRunnable,
600000 30 * 60 * 1000
) )
} }
} }
@@ -550,7 +591,7 @@ object Request {
Log.e(TAG, "token request error $t") Log.e(TAG, "token request error $t")
handler.postDelayed( handler.postDelayed(
tokenRunnable, tokenRunnable,
600000 30 * 60 * 1000
) )
} }
}) })
@@ -599,7 +640,8 @@ object 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>
@@ -632,7 +674,7 @@ object 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> {
@@ -643,7 +685,7 @@ object 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 {
@@ -657,6 +699,31 @@ object 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")
@@ -8,14 +8,7 @@ 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,
private val bootStartup: Boolean,
) :
DialogFragment() {
private var _binding: DialogBinding? = null private var _binding: DialogBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@@ -32,33 +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 ->
SP.channelReversal = isChecked SP.channelReversal = isChecked
(activity as MainActivity).settingActive() (activity as MainActivity).settingActive()
}
} }
val switchChannelNum = _binding?.switchChannelNum binding.switchChannelNum.run {
switchChannelNum?.isChecked = channelNum isChecked = SP.channelNum
switchChannelNum?.setOnCheckedChangeListener { _, isChecked -> setOnCheckedChangeListener { _, isChecked ->
SP.channelNum = isChecked SP.channelNum = isChecked
(activity as MainActivity).settingActive() (activity as MainActivity).settingActive()
}
} }
val switchBootStartup = _binding?.switchBootStartup binding.switchBootStartup.run {
switchBootStartup?.isChecked = bootStartup isChecked = SP.bootStartup
switchBootStartup?.setOnCheckedChangeListener { _, isChecked -> setOnCheckedChangeListener { _, isChecked ->
SP.bootStartup = isChecked SP.bootStartup = isChecked
(activity as MainActivity).settingActive() (activity as MainActivity).settingActive()
}
} }
updateManager = UpdateManager(context, this, versionCode) updateManager = UpdateManager(context, this, context.appVersionCode)
_binding?.checkVersion?.setOnClickListener(OnClickListenerCheckVersion(updateManager)) 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")
} }
} }
@@ -1,7 +1,11 @@
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
@@ -68,6 +72,36 @@ class ApiClient {
.build().create(FAuthService::class.java) .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(
@@ -93,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)
@@ -3,6 +3,7 @@ package com.lizongying.mytv.api
import retrofit2.Call import retrofit2.Call
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header
import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
interface FAuthService { interface FAuthService {
@@ -12,4 +13,11 @@ interface FAuthService {
@Query("live_id") live_id: String = "", @Query("live_id") live_id: String = "",
@Query("live_qa") live_qa: String = "", @Query("live_qa") live_qa: String = "",
): Call<FAuth> ): 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,
)
@@ -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")
}
}
@@ -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: 12 KiB

+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/row_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="0dp"
android:layout_marginStart="10dp"
android:layout_marginBottom="5dp"
android:gravity="start"
android:textColor="#FFEEEEEE"
android:textSize="22sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/row_items"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/show_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</ScrollView>
</FrameLayout>
+1 -1
View File
@@ -130,7 +130,7 @@
"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"
] ]
+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>
+2 -4
View File
@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<integer name="local_channel_version">1</integer> <string name="server_url">http://localhost:8080</string>
<string name="server_version_url">https://fastly.jsdelivr.net/gh/LeGend-wLw/my-tv-json-utils@main/version.txt</string> </resources>
<string name="server_url">https://fastly.jsdelivr.net/gh/LeGend-wLw/my-tv-json-utils@main/channels.json</string>
</resources>
+16 -1
View File
@@ -1,10 +1,25 @@
<resources> <resources>
<style name="CustomImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle"> <style name="CustomImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle">
<item name="lbImageCardViewType">Title|Content</item> <item name="cardType">infoUnder</item>
<item name="infoVisibility">always</item>
<!-- In order to keep backward compatibility we have to create an icon on right. -->
<item name="lbImageCardViewType">Title|Content|IconOnRight</item>
<!-- Deprecated. Use 'Widget.Leanback.ImageCardView.InfoAreaStyle' instead. -->
<item name="infoAreaBackground">@null</item>
</style>
<style name="CustomImageCardViewImageStyle" parent="Widget.Leanback.ImageCardView.ImageStyle">
<item name="android:layout_width">160dp</item>
<item name="android:layout_height">40dp</item>
<item name="android:adjustViewBounds">true</item>
<item name="android:contentDescription">@null</item>
<item name="android:scaleType">centerCrop</item>
<item name="layout_viewType">main</item>
</style> </style>
<style name="CustomImageCardTheme" parent="Theme.Leanback"> <style name="CustomImageCardTheme" parent="Theme.Leanback">
<item name="imageCardViewStyle">@style/CustomImageCardViewStyle</item> <item name="imageCardViewStyle">@style/CustomImageCardViewStyle</item>
<item name="imageCardViewImageStyle">@style/CustomImageCardViewImageStyle</item>
</style> </style>
</resources> </resources>
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