Compare commits

..
26 Commits
Author SHA1 Message Date
Li ZongYing 4fb27ba502 fix channel number did not disappear 2024-02-05 06:12:01 +08:00
李宗英 1714633cb7 Merge pull request #205 from LeGend-wLw/main
移除硬编码,更改为json格式,并支持从服务器上动态更新channels。
2024-02-04 22:46:08 +08:00
LeGend-wLw f7b024f6da 防止多次初始化 2024-02-04 16:42:56 +08:00
LeGend-wLw dd8df63b01 添加初始化TVList行为 2024-02-04 16:42:38 +08:00
LeGend-wLw a4c95efd71 添加从服务器上获取channel.json逻辑,但未启用 2024-02-04 16:37:59 +08:00
LeGend-wLw 9f32fa3673 移除MustToken和needToken,移动到TV类中 2024-02-04 16:37:20 +08:00
LeGend-wLw b3d0093ab0 修改为从json文件中读取channel信息 2024-02-04 16:36:34 +08:00
LeGend-wLw 5798d3a5fe 更改TV类中成员,使能和json对应 2024-02-04 16:36:05 +08:00
LeGend-wLw 4eb90de79d 当前转换成json的channel信息 2024-02-04 16:35:38 +08:00
LeGend-wLw b8232971e2 服务器地址,未更新 2024-02-04 16:35:20 +08:00
LeGend-wLw bc3681b0a6 将从tvModel中获取needToken和MustToken改成从TV类中获取needToken和MustToken 2024-02-04 16:34:59 +08:00
Li ZongYing 088808c3fd fix retry bug 2024-02-03 20:07:08 +08:00
Li ZongYing 73239a6677 replay 2024-02-01 17:04:02 +08:00
Li ZongYing cc2514a81a performance optimization 2024-01-28 16:00:33 +08:00
Li ZongYing bcf9bab25f add channel 2024-01-27 19:52:36 +08:00
Li ZongYing f7eebb3f7a channel allow one num 2024-01-25 12:21:16 +08:00
Li ZongYing 0f4a33a3fc change channel order 2024-01-25 11:37:06 +08:00
Li ZongYing c17d62c698 rename to setting 2024-01-19 13:49:43 +08:00
Li ZongYing 33d3da97e5 fix 13 2024-01-18 22:19:04 +08:00
Li ZongYing ba95ce5374 number selection 2024-01-18 21:42:06 +08:00
Li ZongYing e1ee909071 optimize memu hiding 2024-01-17 10:19:50 +08:00
Li ZongYing 1fd0951f92 channel reversal 2024-01-16 19:04:43 +08:00
Li ZongYing 42b072f5ec support KEYCODE_CHANNEL_UP & KEYCODE_CHANNEL_DOWN 2024-01-14 12:09:12 +08:00
Li ZongYing a23b1e11ce fix the menu did not appear 2024-01-14 10:15:45 +08:00
Li ZongYing 1ac6d87c3c clean log 2024-01-12 10:13:29 +08:00
Li ZongYing b075a7db81 trust all certs 2024-01-11 19:33:31 +08:00
32 changed files with 330 additions and 1991 deletions
-11
View File
@@ -14,16 +14,6 @@
## 更新日志 ## 更新日志
### v1.5.8(通用版)
* 修复央视6画质差的问题
* 增加兵团卫视
* 播放失败重试
### v1.5.6(通用版)
* 解决部分设备系统时间不对导致播放失败的问题
### v1.5.2(通用版) ### v1.5.2(通用版)
* 修复APP恢复后频道号、频道列表不自动消失的问题 * 修复APP恢复后频道号、频道列表不自动消失的问题
@@ -119,7 +109,6 @@ adb install my-tv.apk
* 获取系统时间 * 获取系统时间
* 选中的图标比例能否相差更大 * 选中的图标比例能否相差更大
* 自动重连 * 自动重连
* 节目增加预告
## 赞赏 ## 赞赏
+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.6.4" def retrofit2_version = "2.9.0"
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.
@@ -1,124 +0,0 @@
package com.lizongying.mytv
import android.graphics.Color
import android.view.ContextThemeWrapper
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 owner: LifecycleOwner, private var tvListViewModel: TVListViewModel) :
RecyclerView.Adapter<CardAdapter.ViewHolder>() {
private var listener: ItemListener? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val cardView = object :
ImageCardView(ContextThemeWrapper(parent.context, R.style.CustomImageCardTheme)) {}
startScaleAnimation(cardView, 1.0f, 0.9f)
cardView.isFocusable = true
cardView.isFocusableInTouchMode = true
return ViewHolder(cardView)
}
private fun startScaleAnimation(view: View, fromScale: Float, toScale: Float) {
val scaleAnimation = ScaleAnimation(
fromScale, toScale,
fromScale, toScale,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f
)
scaleAnimation.duration = 200
scaleAnimation.fillAfter = true
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.setOnFocusChangeListener { _, hasFocus ->
listener?.onItemFocusChange(item, hasFocus)
if (hasFocus) {
startScaleAnimation(cardView, 0.9f, 1.0f)
} else {
startScaleAnimation(cardView, 1.0f, 0.9f)
}
}
cardView.setOnClickListener { _ ->
listener?.onItemClicked(item)
}
cardView.titleText = tvViewModel.title.value
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.mainImageView.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 getItemCount() = tvListViewModel.size()
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val view = itemView
}
interface ItemListener {
fun onItemFocusChange(tvViewModel: TVViewModel, hasFocus: Boolean)
fun onItemClicked(tvViewModel: TVViewModel)
}
fun setItemListener(listener: ItemListener) {
this.listener = listener
}
companion object {
private const val TAG = "CardAdapter"
}
}
@@ -47,11 +47,6 @@ class CardPresenter(
.centerInside() .centerInside()
.into(cardView.mainImageView) .into(cardView.mainImageView)
"兵团卫视" -> Glide.with(viewHolder.view.context)
.load(R.drawable.bingtuan)
.centerInside()
.into(cardView.mainImageView)
else -> Glide.with(viewHolder.view.context) else -> Glide.with(viewHolder.view.context)
.load(tvViewModel.logo.value) .load(tvViewModel.logo.value)
.centerInside() .centerInside()
@@ -1,36 +0,0 @@
package com.lizongying.mytv
import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
class ConfirmationDialogFragment(private val listener: ConfirmationDialogListener) :
DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
builder.setTitle("确认")
.setMessage("确认更新吗?")
.setPositiveButton(
"确定"
) { _, _ ->
listener.onConfirm()
}
.setNegativeButton(
"取消"
) { _, _ ->
listener.onCancel()
}
// 创建并返回 AlertDialog 对象
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
interface ConfirmationDialogListener {
fun onConfirm()
fun onCancel()
}
}
@@ -9,8 +9,6 @@ class Encryptor {
external fun hash(data: ByteArray): ByteArray? external fun hash(data: ByteArray): ByteArray?
external fun hash2(data: ByteArray): ByteArray?
companion object { companion object {
init { init {
System.loadLibrary("native") System.loadLibrary("native")
@@ -43,10 +43,6 @@ class InfoFragment : Fragment() {
.load(R.drawable.xinjiang) .load(R.drawable.xinjiang)
.into(binding.infoLogo) .into(binding.infoLogo)
"兵团卫视" -> Glide.with(this)
.load(R.drawable.bingtuan)
.into(binding.infoLogo)
else -> Glide.with(this) else -> Glide.with(this)
.load(tvViewModel.logo.value) .load(tvViewModel.logo.value)
.into(binding.infoLogo) .into(binding.infoLogo)
@@ -18,22 +18,16 @@ 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 import java.security.MessageDigest
class MainActivity : FragmentActivity() { class MainActivity : FragmentActivity() {
private var ready = 0 var playerFragment = PlayerFragment()
private var playerFragment = PlayerFragment() private val mainFragment = MainFragment()
private var mainFragment = MainFragment() private val infoFragment = InfoFragment()
private var infoFragment = InfoFragment() private val channelFragment = ChannelFragment()
private var channelFragment = ChannelFragment()
private lateinit var settingFragment: SettingFragment private lateinit var settingFragment: SettingFragment
private var doubleBackToExitPressedOnce = false private var doubleBackToExitPressedOnce = false
@@ -50,16 +44,6 @@ class MainActivity : FragmentActivity() {
private var versionName = "" 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)
@@ -84,14 +68,8 @@ class MainActivity : FragmentActivity() {
channelReversal = sharedPref.getBoolean(CHANNEL_REVERSAL, channelReversal) channelReversal = sharedPref.getBoolean(CHANNEL_REVERSAL, channelReversal)
channelNum = sharedPref.getBoolean(CHANNEL_NUM, channelNum) channelNum = sharedPref.getBoolean(CHANNEL_NUM, channelNum)
val packageInfo = getPackageInfo() versionName = getPackageInfo().versionName
versionName = packageInfo.versionName settingFragment = SettingFragment(versionName, channelReversal, channelNum)
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) {
@@ -144,7 +122,6 @@ class MainActivity : FragmentActivity() {
val transaction = supportFragmentManager.beginTransaction() val transaction = supportFragmentManager.beginTransaction()
if (mainFragment.isHidden) { if (mainFragment.isHidden) {
mainFragment.setPosition()
transaction.show(mainFragment) transaction.show(mainFragment)
keepRunnable() keepRunnable()
} else { } else {
@@ -178,11 +155,7 @@ class MainActivity : FragmentActivity() {
} }
fun fragmentReady() { fun fragmentReady() {
ready++ mainFragment.fragmentReady()
Log.i(TAG, "ready $ready")
if (ready == 4) {
mainFragment.fragmentReady()
}
} }
override fun onTouchEvent(event: MotionEvent?): Boolean { override fun onTouchEvent(event: MotionEvent?): Boolean {
@@ -522,7 +495,7 @@ class MainActivity : FragmentActivity() {
override fun onResume() { override fun onResume() {
Log.i(TAG, "onResume") Log.i(TAG, "onResume")
super.onResume() super.onResume()
if (!mainFragment.isHidden) { if (!mainFragment.isHidden){
handler.postDelayed(hideMain, delayHideMain) handler.postDelayed(hideMain, delayHideMain)
} }
} }
@@ -5,31 +5,30 @@ import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.leanback.app.BrowseSupportFragment
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 androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.lizongying.mytv.Utils.dpToPx
import com.lizongying.mytv.Utils.getDateTimestamp import com.lizongying.mytv.Utils.getDateTimestamp
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 : Fragment(), CardAdapter.ItemListener { class MainFragment : BrowseSupportFragment() {
private var itemPosition = 0 private var itemPosition = 0
private var rowList: MutableList<View> = mutableListOf() private var rowsAdapter: ArrayObjectAdapter? = null
private var _binding: ShowBinding? = null
private val binding get() = _binding!!
private var request = Request() private var request = Request()
@@ -42,12 +41,17 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private lateinit var mUpdateProgramRunnable: UpdateProgramRunnable private lateinit var mUpdateProgramRunnable: UpdateProgramRunnable
override fun onCreateView( private var ready = 0
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?) {
@@ -56,142 +60,172 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
activity?.let { request.initYSP(it) } activity?.let { request.initYSP(it) }
sharedPref = (activity as? MainActivity)?.sharedPref!! sharedPref = (activity as? MainActivity)?.sharedPref!!
view?.post { loadRows()
val content = binding.content
var idx: Long = 0 setupEventListeners()
for ((k, v) in TVList.list) {
val itemBinding: RowBinding =
RowBinding.inflate(layoutInflater, content, false)
val tvListViewModelCurrent = TVListViewModel() mUpdateProgramRunnable = UpdateProgramRunnable()
for ((idx2, v1) in v.withIndex()) { handler.post(mUpdateProgramRunnable)
val tvViewModel = TVViewModel(v1)
tvViewModel.setRowPosition(idx.toInt()) tvListViewModel.tvListViewModel.value?.forEach { tvViewModel ->
tvViewModel.setItemPosition(idx2) tvViewModel.errInfo.observe(viewLifecycleOwner) { _ ->
tvListViewModelCurrent.addTVViewModel(tvViewModel) if (tvViewModel.errInfo.value != null
tvListViewModel.addTVViewModel(tvViewModel) && tvViewModel.id.value == itemPosition
) {
Toast.makeText(context, tvViewModel.errInfo.value, Toast.LENGTH_SHORT).show()
} }
tvListViewModel.maxNum.add(v.size)
val adapter = CardAdapter(viewLifecycleOwner, tvListViewModelCurrent)
rowList.add(itemBinding.rowItems)
adapter.setItemListener(this)
itemBinding.rowHeader.text = k
itemBinding.rowItems.adapter = adapter
itemBinding.rowItems.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
val layoutParams = itemBinding.row.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.topMargin = dpToPx(11F)
itemBinding.row.layoutParams = layoutParams
content.addView(itemBinding.row)
idx++
} }
tvViewModel.ready.observe(viewLifecycleOwner) { _ ->
mUpdateProgramRunnable = UpdateProgramRunnable() // not first time && channel not change
handler.post(mUpdateProgramRunnable) if (tvViewModel.ready.value != null
&& tvViewModel.id.value == itemPosition
itemPosition = sharedPref.getInt(POSITION, 0) && check(tvViewModel)
if (itemPosition >= tvListViewModel.size()) { ) {
itemPosition = 0 Log.i(TAG, "ready ${tvViewModel.title.value}")
(activity as? MainActivity)?.play(tvViewModel)
}
} }
tvListViewModel.setItemPosition(itemPosition) tvViewModel.change.observe(viewLifecycleOwner) { _ ->
setPosition() if (tvViewModel.change.value != null) {
tvListViewModel.tvListViewModel.value?.forEach { tvViewModel -> val title = tvViewModel.title.value
tvViewModel.errInfo.observe(viewLifecycleOwner) { _ -> if (tvViewModel.pid.value != "") {
if (tvViewModel.errInfo.value != null Log.i(TAG, "request $title")
&& tvViewModel.id.value == itemPosition lifecycleScope.launch(Dispatchers.IO) {
) { tvViewModel.let { request.fetchData(it) }
Toast.makeText(context, tvViewModel.errInfo.value, Toast.LENGTH_SHORT) }
.show() (activity as? MainActivity)?.showInfoFragment(tvViewModel)
} setSelectedPosition(
} tvViewModel.getRowPosition(), true,
tvViewModel.ready.observe(viewLifecycleOwner) { _ -> SelectItemViewHolderTask(tvViewModel.getItemPosition())
)
// not first time && channel not change } else {
if (tvViewModel.ready.value != null if (check(tvViewModel)) {
&& tvViewModel.id.value == itemPosition (activity as? MainActivity)?.play(tvViewModel)
&& 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) (activity as? MainActivity)?.showInfoFragment(tvViewModel)
setPosition( setSelectedPosition(
tvViewModel.getRowPosition(), tvViewModel.getItemPosition() tvViewModel.getRowPosition(), true,
SelectItemViewHolderTask(tvViewModel.getItemPosition())
) )
} else {
if (check(tvViewModel)) {
(activity as? MainActivity)?.play(tvViewModel)
(activity as? MainActivity)?.showInfoFragment(tvViewModel)
setPosition(
tvViewModel.getRowPosition(), tvViewModel.getItemPosition()
)
}
} }
} }
} }
} }
}
(activity as MainActivity).fragmentReady() 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
context?.let { TVList.init(it) }
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 = sharedPref.getInt(POSITION, 0)
if (itemPosition >= tvListViewModel.size()) {
itemPosition = 0
}
tvListViewModel.setItemPosition(itemPosition)
}
fun prevSource() {
view?.post {
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
if (tvViewModel != null) {
if (tvViewModel.videoUrl.value!!.size > 1) {
val videoIndex = tvViewModel.videoIndex.value?.minus(1)
if (videoIndex == -1) {
tvViewModel.setVideoIndex(tvViewModel.videoUrl.value!!.size - 1)
}
tvViewModel.changed()
}
}
} }
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { fun nextSource() {
super.onViewCreated(view, savedInstanceState) view?.post {
} val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
if (tvViewModel != null) {
override fun onItemFocusChange(tvViewModel: TVViewModel, hasFocus: Boolean) { if (tvViewModel.videoUrl.value!!.size > 1) {
if (hasFocus) { val videoIndex = tvViewModel.videoIndex.value?.plus(1)
tvListViewModel.setItemPositionCurrent(tvViewModel.id.value!!) if (videoIndex == tvViewModel.videoUrl.value!!.size) {
(activity as MainActivity).keepRunnable() tvViewModel.setVideoIndex(0)
}
tvViewModel.changed()
}
}
} }
} }
override fun onItemClicked(tvViewModel: TVViewModel) { private fun setupEventListeners() {
if (this.isHidden) { onItemViewClickedListener = ItemViewClickedListener()
(activity as? MainActivity)?.switchMainFragment() onItemViewSelectedListener = ItemViewSelectedListener()
return
}
if (itemPosition != tvViewModel.id.value!!) {
itemPosition = tvViewModel.id.value!!
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVViewModel(itemPosition)?.changed()
}
(activity as? MainActivity)?.switchMainFragment()
} }
fun setPosition() { private inner class ItemViewClickedListener : OnItemViewClickedListener {
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition) override fun onItemClicked(
Log.i(TAG, "tvViewModel $tvViewModel") itemViewHolder: Presenter.ViewHolder,
Log.i(TAG, "rowList ${rowList.size}") item: Any,
rowList[tvViewModel!!.getRowPosition()].post { rowViewHolder: RowPresenter.ViewHolder,
((rowList[tvViewModel.getRowPosition()] as RecyclerView).layoutManager as LinearLayoutManager).findViewByPosition( row: Row
tvViewModel.getItemPosition() ) {
)?.requestFocus() if (item is TVViewModel) {
if (itemPosition != item.id.value!!) {
itemPosition = item.id.value!!
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVViewModel(itemPosition)?.changed()
}
(activity as? MainActivity)?.switchMainFragment()
}
} }
} }
fun setPosition(rowPosition: Int, itemPosition: Int) { private inner class ItemViewSelectedListener : OnItemViewSelectedListener {
rowList[rowPosition].post { override fun onItemSelected(
((rowList[rowPosition] as RecyclerView).layoutManager as LinearLayoutManager).findViewByPosition( itemViewHolder: Presenter.ViewHolder?, item: Any?,
itemPosition rowViewHolder: RowPresenter.ViewHolder, row: Row
)?.requestFocus() ) {
if (item is TVViewModel) {
tvListViewModel.setItemPositionCurrent(item.id.value!!)
(activity as MainActivity).keepRunnable()
}
} }
} }
@@ -212,8 +246,12 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
} }
fun fragmentReady() { fun fragmentReady() {
ready++
Log.i(TAG, "ready $ready")
if (ready == 4) {
// request.fetchPage() // request.fetchPage()
tvListViewModel.getTVViewModel(itemPosition)?.changed() tvListViewModel.getTVViewModel(itemPosition)?.changed()
}
} }
fun play(itemPosition: Int) { fun play(itemPosition: Int) {
@@ -295,15 +333,7 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
override fun onDestroy() { override fun onDestroy() {
Log.i(TAG, "onDestroy") Log.i(TAG, "onDestroy")
super.onDestroy() super.onDestroy()
if (::mUpdateProgramRunnable.isInitialized) { handler.removeCallbacks(mUpdateProgramRunnable)
handler.removeCallbacks(mUpdateProgramRunnable)
}
}
override fun onDestroyView() {
Log.i(TAG, "onDestroyView")
super.onDestroyView()
_binding = null
} }
companion object { companion object {
@@ -32,6 +32,7 @@ class PlayerFragment : Fragment() {
): View { ): View {
_binding = PlayerBinding.inflate(inflater, container, false) _binding = PlayerBinding.inflate(inflater, container, false)
playerView = _binding!!.playerView playerView = _binding!!.playerView
(activity as MainActivity).playerFragment = this
playerView?.viewTreeObserver?.addOnGlobalLayoutListener(object : playerView?.viewTreeObserver?.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener { ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() { override fun onGlobalLayout() {
@@ -62,7 +63,6 @@ class PlayerFragment : Fragment() {
super.onPlayerError(error) super.onPlayerError(error)
Log.e(TAG, "PlaybackException $error") Log.e(TAG, "PlaybackException $error")
tvViewModel?.changed()
} }
}) })
} }
@@ -86,7 +86,6 @@ class PlayerFragment : Fragment() {
if (playerView != null && playerView!!.player?.isPlaying == false) { if (playerView != null && playerView!!.player?.isPlaying == false) {
Log.i(TAG, "replay") Log.i(TAG, "replay")
playerView!!.player?.prepare() playerView!!.player?.prepare()
playerView!!.player?.play()
} else { } else {
Log.i(TAG, "playing") Log.i(TAG, "playing")
} }
+79 -150
View File
@@ -7,8 +7,6 @@ import android.util.Base64
import android.util.Log import android.util.Log
import com.lizongying.mytv.Utils.getDateFormat 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.AuthRequest
import com.lizongying.mytv.api.Info import com.lizongying.mytv.api.Info
import com.lizongying.mytv.api.LiveInfo import com.lizongying.mytv.api.LiveInfo
import com.lizongying.mytv.api.LiveInfoRequest import com.lizongying.mytv.api.LiveInfoRequest
@@ -43,7 +41,55 @@ class Request {
private val regex = Regex("""des_key = "([^"]+).+var des_iv = "([^"]+)""") private val regex = Regex("""des_key = "([^"]+).+var des_iv = "([^"]+)""")
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,AppleM1Pro,OpenGL4.1)"}""".toByteArray()
private var mapping = mapOf(
"CCTV4K" to "CCTV4K 超高清",
"CCTV1" to "CCTV1 综合",
"CCTV2" to "CCTV2 财经",
"CCTV4" to "CCTV4 中文国际",
"CCTV5" to "CCTV5 体育",
"CCTV5+" to "CCTV5+ 体育赛事",
"CCTV7" to "CCTV7 国防军事",
"CCTV9" to "CCTV9 记录",
"CCTV10" to "CCTV10 科教",
"CCTV11" to "CCTV11 戏曲",
"CCTV12" to "CCTV12 社会与法",
"CCTV13" to "CCTV13 新闻",
"CCTV14" to "CCTV14 少儿",
"CCTV15" to "CCTV15 音乐",
"CCTV16-HD" to "CCTV16 奥林匹克",
"CCTV17" to "CCTV17 农业农村",
"CGTN" to "CGTN",
"CGTN法语频道" to "CGTN 法语频道",
"CGTN俄语频道" to "CGTN 俄语频道",
"CGTN阿拉伯语频道" to "CGTN 阿拉伯语频道",
"CGTN西班牙语频道" to "CGTN 西班牙语频道",
"CGTN外语纪录频道" to "CGTN 纪录频道",
"东方卫视" to "东方卫视",
"湖南卫视" to "湖南卫视",
"湖北卫视" to "湖北卫视",
"辽宁卫视" to "辽宁卫视",
"江苏卫视" to "江苏卫视",
"江西卫视" to "江西卫视",
"山东卫视" to "山东卫视",
"广东卫视" to "广东卫视",
"广西卫视" to "广西卫视",
"重庆卫视" to "重庆卫视",
"河南卫视" to "河南卫视",
"河北卫视" to "河北卫视",
"贵州卫视" to "贵州卫视",
"北京卫视" to "北京卫视",
"黑龙江卫视" to "黑龙江卫视",
"浙江卫视" to "浙江卫视",
"安徽卫视" to "安徽卫视",
"深圳卫视" to "深圳卫视",
"四川卫视" to "四川卫视",
"福建东南卫视" to "东南卫视",
"海南卫视" to "海南卫视",
"天津卫视" to "天津卫视",
"新疆卫视" to "新疆卫视",
)
init { init {
handler.post(tokenRunnable) handler.post(tokenRunnable)
@@ -51,87 +97,11 @@ class Request {
fun initYSP(context: Context) { fun initYSP(context: Context) {
ysp = YSP(context) ysp = YSP(context)
//TODO 不确定在哪里初始化
TVList.init(context)
} }
var call: Call<LiveInfo>? = null var call: Call<LiveInfo>? = null
var callAuth: Call<Auth>? = null
private fun fetchAuth(tvModel: TVViewModel, cookie: String) {
callAuth?.cancel()
val title = tvModel.title.value
val data = ysp?.getAuthData(tvModel)
val request = data?.let { AuthRequest(it) }
callAuth = request?.let { yspApiService.getAuth("guid=${ysp?.getGuid()}; $cookie", it) }
callAuth?.enqueue(object : Callback<Auth> {
override fun onResponse(call: Call<Auth>, response: Response<Auth>) {
if (response.isSuccessful) {
val liveInfo = response.body()
if (liveInfo?.data?.token != null) {
Log.i(TAG, "token ${liveInfo.data.token}")
ysp?.token = liveInfo.data.token
fetchVideo(tvModel, cookie)
} else {
Log.e(TAG, "$title token error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) {
if (!tvModel.getTV().mustToken) {
fetchAuth(tvModel, cookie)
}
} else {
token = ""
fetchAuth(tvModel)
}
} else {
fetchAuth(tvModel, cookie)
}
}
}
} else {
Log.e(TAG, "$title auth status error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) {
if (!tvModel.getTV().mustToken) {
fetchAuth(tvModel, cookie)
}
} else {
token = ""
fetchAuth(tvModel)
}
} else {
fetchAuth(tvModel, cookie)
}
}
}
}
override fun onFailure(call: Call<Auth>, t: Throwable) {
Log.e(TAG, "$title auth request error $t")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) {
if (!tvModel.getTV().mustToken) {
fetchVideo(tvModel, cookie)
}
} else {
token = ""
fetchAuth(tvModel)
}
} else {
fetchAuth(tvModel, cookie)
}
}
}
})
}
fun fetchVideo(tvModel: TVViewModel, cookie: String) { fun fetchVideo(tvModel: TVViewModel, cookie: String) {
call?.cancel() call?.cancel()
@@ -144,13 +114,12 @@ class Request {
tvModel.seq = 0 tvModel.seq = 0
val data = ysp?.switch(tvModel) val data = ysp?.switch(tvModel)
val request = data?.let { LiveInfoRequest(it) } val request = data?.let { LiveInfoRequest(it) }
call = request?.let { yspApiService.getLiveInfo("guid=${ysp?.getGuid()}; $cookie", ysp!!.token, it) } call = request?.let { yspApiService.getLiveInfo("guid=${ysp?.getGuid()}; $cookie", 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>) {
if (response.isSuccessful) { if (response.isSuccessful) {
val liveInfo = response.body() val liveInfo = response.body()
if (liveInfo?.data?.playurl != null) { if (liveInfo?.data?.playurl != null) {
val chanll = liveInfo.data.chanll val chanll = liveInfo.data.chanll
val decodedBytes = Base64.decode( val decodedBytes = Base64.decode(
@@ -166,8 +135,8 @@ class Request {
val url = liveInfo.data.playurl + "&revoi=" + encryptTripleDES( val url = liveInfo.data.playurl + "&revoi=" + encryptTripleDES(
keyBytes + byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0), keyBytes + byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0),
ivBytes ivBytes
).uppercase() + liveInfo.data.extended_param ).uppercase()
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.retryTimes = 0
@@ -255,53 +224,6 @@ class Request {
}) })
} }
fun fetchAuth(tvModel: TVViewModel) {
if (token == "") {
yspTokenService.getInfo()
.enqueue(object : Callback<Info> {
override fun onResponse(call: Call<Info>, response: Response<Info>) {
if (response.isSuccessful) {
token = response.body()?.data?.token!!
Log.i(TAG, "info success $token")
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"
fetchAuth(tvModel, cookie)
} else {
Log.e(TAG, "info status error")
if (tvModel.tokenRetryTimes < tvModel.tokenRetryMaxTimes) {
tvModel.tokenRetryTimes++
fetchAuth(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)
}
}
}
}
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 {
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 {
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"
fetchAuth(tvModel, cookie)
}
}
fun fetchVideo(tvModel: TVViewModel) { fun fetchVideo(tvModel: TVViewModel) {
if (token == "") { if (token == "") {
yspTokenService.getInfo() yspTokenService.getInfo()
@@ -311,7 +233,7 @@ class Request {
token = response.body()?.data?.token!! token = response.body()?.data?.token!!
Log.i(TAG, "info success $token") Log.i(TAG, "info success $token")
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; yspappid=519748109;yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
} else { } else {
Log.e(TAG, "info status error") Log.e(TAG, "info status error")
@@ -321,7 +243,7 @@ class Request {
} else { } else {
if (!tvModel.getTV().mustToken) { if (!tvModel.getTV().mustToken) {
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; yspappid=519748109"
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
} }
} }
@@ -336,7 +258,7 @@ class Request {
} else { } else {
if (!tvModel.getTV().mustToken) { if (!tvModel.getTV().mustToken) {
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; yspappid=519748109"
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
} }
} }
@@ -344,20 +266,18 @@ class Request {
}) })
} 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; yspappid=519748109;yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
} }
} }
fun fetchData(tvModel: TVViewModel) { fun fetchData(tvModel: TVViewModel) {
if (tvModel.getTV().needToken) { if (tvModel.getTV().needToken) {
// fetchVideo(tvModel) fetchVideo(tvModel)
fetchAuth(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; yspappid=519748109"
// fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
fetchAuth(tvModel, cookie)
} }
} }
@@ -436,20 +356,29 @@ class Request {
) { ) {
if (response.isSuccessful) { if (response.isSuccessful) {
val body = response.body() val body = response.body()
if (body?.data?.feedModuleListCount == 1) { if (body?.data?.feedModuleListCount == 1) {
for (item in body.data?.feedModuleListList!![0]?.dataTvChannelListList!!) { for (item in body.data?.feedModuleListList!![0]?.dataTvChannelListList!!) {
Log.d( if (item.isVip && !item.isLimitedFree) {
continue
}
Log.i(
TAG, TAG,
"${item.channelName},${item.pid},${item.streamId}" "${item.channelName},${item.pid},${item.streamId}"
) )
var channelType = "央视频道"
for ((_, v) in TVList.list) { if (item?.channelType === "weishi") {
for (v2 in v) { channelType = "地方频道"
if (v2.title == item.channelName || v2.alias == item.channelName) { }
v2.pid = item.pid if (!mapping.containsKey(item.channelName)) {
v2.sid = item.streamId continue
} }
} val tv =
TVList.list[channelType]?.find { it.title == mapping[item.channelName] }
if (tv != null) {
tv.logo = item.tvLogo
tv.pid = item.pid
tv.sid = item.streamId
} }
} }
} }
@@ -1,28 +1,22 @@
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(private val versionName: String,
private val versionName: String, private val channelReversal: Boolean,
private val versionCode: Long, private val channelNum: Boolean,
private val channelReversal: Boolean, ) :
private val channelNum: Boolean,
) :
DialogFragment() { DialogFragment() {
private var _binding: DialogBinding? = null private var _binding: DialogBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var updateManager: UpdateManager
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0) setStyle(STYLE_NO_TITLE, 0)
@@ -49,33 +43,14 @@ class SettingFragment(
(activity as MainActivity).saveChannelNum(isChecked) (activity as MainActivity).saveChannelNum(isChecked)
} }
updateManager = UpdateManager(context, this, versionCode)
_binding?.checkVersion?.setOnClickListener(OnClickListenerCheckVersion(updateManager))
return binding.root return binding.root
} }
fun setVersionName(versionName: String) {
binding.versionName.text = versionName
}
internal class OnClickListenerCheckVersion(private val updateManager: UpdateManager) :
View.OnClickListener {
override fun onClick(view: View?) {
updateManager.checkAndUpdate()
}
}
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
_binding = null _binding = null
} }
override fun onDestroy() {
super.onDestroy()
// updateManager.destroy()
}
companion object { companion object {
const val TAG = "SettingFragment" const val TAG = "SettingFragment"
} }
+3 -4
View File
@@ -5,7 +5,6 @@ import java.io.Serializable
data class TV( data class TV(
var id: Int = 0, var id: Int = 0,
var title: String, var title: String,
var alias: String = "",
var videoUrl: List<String>, var videoUrl: List<String>,
var videoIndex: Int = 0, var videoIndex: Int = 0,
var channel: String = "", var channel: String = "",
@@ -13,10 +12,10 @@ data class TV(
var pid: String = "", var pid: String = "",
var sid: String = "", var sid: String = "",
var programId: String = "", var programId: String = "",
var needToken: Boolean = false, var needToken:Boolean=false,
var mustToken: Boolean = false, var mustToken:Boolean=false,
) : Serializable { ) : Serializable {
override fun toString(): String { override fun toString(): String {
return "TV{" + return "TV{" +
File diff suppressed because it is too large Load Diff
@@ -1,189 +0,0 @@
package com.lizongying.mytv
import android.app.DownloadManager
import android.app.DownloadManager.Request
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Toast
import com.lizongying.mytv.api.Release
import com.lizongying.mytv.requests.MyRequest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
class UpdateManager(
private var context: Context?,
private var settingFragment: SettingFragment,
private var versionCode: Long
) :
ConfirmationDialogFragment.ConfirmationDialogListener {
private var myRequest = MyRequest()
private var release: Release? = null
private var downloadReceiver: DownloadReceiver? = null
fun checkAndUpdate() {
CoroutineScope(Dispatchers.Main).launch {
try {
release = myRequest.getRelease()
updateUI(release)
Log.i(TAG, "versionCode $versionCode ${release?.data?.versionCode}")
if (release != null) {
if (release?.data?.versionCode!! >= versionCode) {
val dialog = ConfirmationDialogFragment(this@UpdateManager)
dialog.show(settingFragment.fragmentManager, "ConfirmationDialogFragment")
} else {
Toast.makeText(context, "不需要更新", Toast.LENGTH_LONG)
.show()
}
}
} catch (e: Exception) {
Log.e(TAG, "Error occurred: ${e.message}", e)
}
}
}
private fun updateUI(release: Release?) {
if (release?.data?.versionName.isNullOrEmpty()) {
settingFragment.setVersionName("版本获取失败")
} else {
settingFragment.setVersionName("最新版本:${release?.data?.versionName!!}")
}
}
private fun startDownload(release: Release) {
val apkFileName = "my-tv-${release.data.versionName}.apk"
Log.i(TAG, "apkFileName $apkFileName")
val downloadManager =
context!!.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = Request(Uri.parse(release.data.downloadUrl))
Log.i(TAG, "url ${Uri.parse(release.data.downloadUrl)}")
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, apkFileName)
request.setTitle("New Version Download")
request.setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
// 获取下载任务的引用
val downloadReference = downloadManager.enqueue(request)
downloadReceiver = DownloadReceiver(context!!, apkFileName, downloadReference)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context!!.registerReceiver(
downloadReceiver,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
Context.RECEIVER_NOT_EXPORTED,
)
} else {
context!!.registerReceiver(
downloadReceiver,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
)
}
getDownloadProgress(context!!, downloadReference) { progress ->
println("Download progress: $progress%")
}
}
private fun getDownloadProgress(context: Context, downloadId: Long, progressListener: (Int) -> Unit) {
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val handler = Handler(Looper.getMainLooper())
val intervalMillis: Long = 1000
handler.post(object : Runnable {
override fun run() {
Log.i(TAG, "search")
val query = DownloadManager.Query().setFilterById(downloadId)
val cursor: Cursor = downloadManager.query(query)
cursor.use {
if (it.moveToFirst()) {
val bytesDownloadedIndex =
it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
val bytesTotalIndex =
it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
// 检查列名是否存在
if (bytesDownloadedIndex != -1 && bytesTotalIndex != -1) {
val bytesDownloaded = it.getInt(bytesDownloadedIndex)
val bytesTotal = it.getInt(bytesTotalIndex)
if (bytesTotal != -1) {
val progress = (bytesDownloaded * 100L / bytesTotal).toInt()
progressListener(progress)
if (progress == 100) {
return
}
}
}
}
}
// handler.postDelayed(this, intervalMillis)
}
})
}
private class DownloadReceiver(
private val context: Context,
private val apkFileName: String,
private val downloadReference: Long
) : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val reference = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
Log.i(TAG, "reference $reference")
val progress = intent.getIntExtra("progress", 0)
Log.i(TAG, "progress $progress")
// 检查是否是我们发起的下载
if (reference == downloadReference) {
// 下载完成,触发安装
installNewVersion()
}
}
private fun installNewVersion() {
val installIntent = Intent(Intent.ACTION_VIEW)
val apkUri = Uri.fromFile(
File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
apkFileName
)
)
installIntent.setDataAndType(apkUri, "application/vnd.android.package-archive")
installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(installIntent)
}
}
companion object {
private const val TAG = "UpdateManager"
}
override fun onConfirm() {
Log.i(TAG, "onConfirm $release")
release?.let { startDownload(it) }
}
override fun onCancel() {
}
fun destroy() {
if (downloadReceiver != null) {
context!!.unregisterReceiver(downloadReceiver)
Log.i(TAG, "destroy downloadReceiver")
}
}
}
+4 -49
View File
@@ -3,63 +3,18 @@ package com.lizongying.mytv
import android.content.Context import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.util.TypedValue import android.util.TypedValue
import com.google.gson.Gson
import com.lizongying.mytv.api.TimeResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.*
import java.util.Locale import java.io.IOException
object Utils { object Utils {
private var between: Long = 0
fun getDateFormat(format: String): String { fun getDateFormat(format: String): String {
return SimpleDateFormat( return SimpleDateFormat(format, Locale.CHINA).format(Date())
format,
Locale.CHINA
).format(Date(System.currentTimeMillis() - between))
} }
fun getDateTimestamp(): Long { fun getDateTimestamp(): Long {
return (System.currentTimeMillis() - between) / 1000 return Date().time / 1000
}
suspend fun init() {
var currentTimeMillis: Long = 0
try {
currentTimeMillis = getTimestampFromServer()
} catch (e: Exception) {
println("Failed to retrieve timestamp from server: ${e.message}")
}
between = System.currentTimeMillis() - currentTimeMillis
}
/**
* 从服务器获取时间戳
* @return Long 时间戳
*/
private suspend fun getTimestampFromServer(): Long {
return 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("https://api.m.taobao.com/rest/api3.do?api=mtop.common.getTimestamp")
.build()
try {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val string = response.body()?.string()
Gson().fromJson(string, TimeResponse::class.java).data.t.toLong()
}
} catch (e: IOException) {
// Handle network errors
throw IOException("Error during network request", e)
}
}
} }
fun dpToPx(dp: Float): Int { fun dpToPx(dp: Float): Int {
@@ -5,8 +5,6 @@ import okhttp3.OkHttpClient
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
@@ -15,11 +13,9 @@ import javax.net.ssl.X509TrustManager
class ApiClient { class ApiClient {
private val yspUrl = "https://player-api.yangshipin.cn/" private val yspUrl = "https://player-api.yangshipin.cn/"
private val myUrl = "https://lyrics.run/" private val myUrl = "https://lyrics.run/"
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 var okHttpClient = getUnsafeOkHttpClient() private var okHttpClient = getUnsafeOkHttpClient()
val yspApiService: YSPApiService by lazy { val yspApiService: YSPApiService by lazy {
@@ -38,14 +34,6 @@ class ApiClient {
.build().create(YSPTokenService::class.java) .build().create(YSPTokenService::class.java)
} }
val releaseService: ReleaseService by lazy {
Retrofit.Builder()
.baseUrl(devUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build().create(ReleaseService::class.java)
}
val yspProtoService: YSPProtoService by lazy { val yspProtoService: YSPProtoService by lazy {
Retrofit.Builder() Retrofit.Builder()
.baseUrl(protoUrl) .baseUrl(protoUrl)
@@ -1,37 +0,0 @@
package com.lizongying.mytv.api
import android.util.Log
import okhttp3.MediaType
import okhttp3.RequestBody
import okio.BufferedSink
import java.io.IOException
data class Auth(
val code: Int,
val msg: String,
val data: AuthData,
)
data class AuthData(
val token: String,
)
data class AuthRequest(
var data: String,
) : RequestBody() {
override fun contentType(): MediaType? {
return MediaType.parse("application/x-www-form-urlencoded;charset=UTF-8")
}
override fun writeTo(sink: BufferedSink) {
try {
sink.writeUtf8(data)
} catch (e: IOException) {
Log.e(TAG, "$e")
}
}
companion object {
private const val TAG = "AuthRequest"
}
}
@@ -3,30 +3,9 @@ package com.lizongying.mytv.api
data class Info( data class Info(
val code: Int?, val code: Int?,
val msg: String?, val msg: String?,
val data: Data, val data: InfoData,
) { )
data class Data(
val token: String,
)
}
data class Release( data class InfoData(
val code: Int?, val token: String,
val msg: String?, )
val data: Data,
) {
data class Data(
val versionName: String,
val versionCode: Int,
val downloadUrl: String,
val updateTime: Int,
)
}
data class TimeResponse(
val data: Time
) {
data class Time(
val t: String
)
}
@@ -13,7 +13,6 @@ data class LiveInfo(
) )
data class LiveInfoData( data class LiveInfoData(
val extended_param: String,
val chanll: String, val chanll: String,
val playurl: String, val playurl: String,
val errinfo: String, val errinfo: String,
@@ -1,11 +0,0 @@
package com.lizongying.mytv.api
import retrofit2.Call
import retrofit2.http.GET
interface ReleaseService {
@GET("my-tv/v1/release")
fun getRelease(
): Call<Release>
}
@@ -50,9 +50,6 @@ class YSP(var context: Context) {
private var signature = "" private var signature = ""
private var appid = "ysp_pc"
var token = ""
private var encryptor: Encryptor? = null private var encryptor: Encryptor? = null
private lateinit var sharedPref: SharedPreferences private lateinit var sharedPref: SharedPreferences
@@ -86,19 +83,6 @@ class YSP(var context: Context) {
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"}"""
} }
fun getAuthData(tvModel: TVViewModel): String {
livepid = tvModel.pid.value!!
randStr = getRand()
if (tvModel.retryTimes > 0) {
guid = newGuid()
}
signature = getAuthSignature()
return """pid=$livepid&guid=$guid&appid=$appid&rand_str=$randStr&signature=$signature"""
}
private fun getTimeStr(): String { private fun getTimeStr(): String {
return getDateTimestamp().toString() return getDateTimestamp().toString()
} }
@@ -143,14 +127,8 @@ 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 ""
return hashedData.let { it -> it.joinToString("") { "%02x".format(it) } }
}
private fun getAuthSignature(): String { val hashedData = encryptor?.hash(e) ?: return ""
val e =
"appid=${appid}&guid=${guid}&pid=${livepid}&rand_str=${randStr}".toByteArray()
val hashedData = encryptor?.hash2(e) ?: return ""
return hashedData.let { it -> it.joinToString("") { "%02x".format(it) } } return hashedData.let { it -> it.joinToString("") { "%02x".format(it) } }
} }
@@ -13,23 +13,9 @@ interface YSPApiService {
"content-type: application/json;charset=UTF-8", "content-type: application/json;charset=UTF-8",
"referer: https://www.yangshipin.cn/", "referer: https://www.yangshipin.cn/",
"yspappid: 519748109", "yspappid: 519748109",
"user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
) )
fun getLiveInfo( fun getLiveInfo(
@Header("cookie") cookie: String, @Header("cookie") cookie: String,
@Header("Yspplayertoken") token: String,
@Body request: LiveInfoRequest, @Body request: LiveInfoRequest,
): Call<LiveInfo> ): Call<LiveInfo>
@POST("v1/player/auth")
@Headers(
"content-type: application/x-www-form-urlencoded;charset=UTF-8",
"referer: https://www.yangshipin.cn/",
"yspappid: 519748109",
"user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
)
fun getAuth(
@Header("cookie") cookie: String,
@Body request: AuthRequest,
): Call<Auth>
} }
@@ -1,44 +0,0 @@
package com.lizongying.mytv.requests
import com.lizongying.mytv.api.ApiClient
import com.lizongying.mytv.api.Release
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class MyRequest {
private var releaseService = ApiClient().releaseService
suspend fun getRelease(): Release? {
return withContext(Dispatchers.IO) {
fetchRelease()
}
}
private suspend fun fetchRelease(): Release? {
return suspendCoroutine { continuation ->
releaseService.getRelease()
.enqueue(object : Callback<Release> {
override fun onResponse(call: Call<Release>, response: Response<Release>) {
if (response.isSuccessful) {
continuation.resume(response.body())
} else {
continuation.resume(null)
}
}
override fun onFailure(call: Call<Release>, t: Throwable) {
continuation.resume(null)
}
})
}
}
companion object {
private const val TAG = "MyRequest"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

+1 -25
View File
@@ -27,31 +27,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:text="当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/" android:text="当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/"
/> ></TextView>
<LinearLayout
android:layout_width="300dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<Button
android:id="@+id/check_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="@string/check_version"
/>
<ProgressBar
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/version_name"
android:text=""
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<Switch <Switch
android:id="@+id/switch_channel_reversal" android:id="@+id/switch_channel_reversal"
android:text="@string/title_channel_reversal" android:text="@string/title_channel_reversal"
-25
View File
@@ -1,25 +0,0 @@
<?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="match_parent"
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
@@ -1,17 +0,0 @@
<?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
View File
@@ -2,5 +2,4 @@
<string name="app_name">我的电视</string> <string name="app_name">我的电视</string>
<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>
</resources> </resources>
+1 -16
View File
@@ -1,25 +1,10 @@
<resources> <resources>
<style name="CustomImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle"> <style name="CustomImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle">
<item name="cardType">infoUnder</item> <item name="lbImageCardViewType">Title|Content</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">143dp</item>
<item name="android:layout_height">50dp</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>