Compare commits

...
13 Commits
Author SHA1 Message Date
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
Li ZongYing 7821e8fa58 optimize 2024-01-11 15:19:57 +08:00
Li ZongYing 3109a61bcf action sign 2024-01-10 23:19:21 +08:00
Li ZongYing ce6e5b6546 action sign 2024-01-10 23:18:16 +08:00
Li ZongYing 230e4fea2b action sign 2024-01-10 23:14:49 +08:00
Li ZongYing 0d26912321 action sign 2024-01-10 23:12:35 +08:00
Li ZongYing af9ff5c8dd action sign 2024-01-10 22:39:49 +08:00
Li ZongYing f0f43e0fda dns cache 2024-01-10 22:11:13 +08:00
Li ZongYing 61f62121f2 menu auto close 2024-01-10 18:01:25 +08:00
Li ZongYing d0c7627e35 fix 13 2024-01-08 22:40:56 +08:00
23 changed files with 850 additions and 210 deletions
+3
View File
@@ -33,6 +33,9 @@ jobs:
signingKeyBase64: ${{ secrets.KEYSTORE }} signingKeyBase64: ${{ secrets.KEYSTORE }}
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
keyPassword: ${{ secrets.ALIAS_PASSWORD }} keyPassword: ${{ secrets.ALIAS_PASSWORD }}
env:
# override default build-tools version (29.0.3) -- optional
BUILD_TOOLS_VERSION: "34.0.0"
- name: Create Release - name: Create Release
id: create_release id: create_release
+5
View File
@@ -14,6 +14,11 @@
## 更新日志 ## 更新日志
### v1.3.3
* 部分错误会提示用户
* 菜单3秒钟后自动关闭
### v1.3.2 ### v1.3.2
* 增加重试,减少因网络问题导致的播放失败 * 增加重试,减少因网络问题导致的播放失败
+6 -3
View File
@@ -100,10 +100,13 @@ dependencies {
// For HLS playback support with ExoPlayer // For HLS playback support with ExoPlayer
implementation "androidx.media3:media3-exoplayer-hls:$media3_version" implementation "androidx.media3:media3-exoplayer-hls:$media3_version"
// 21:2.9.0 17:2.6.4
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:2.9.0' implementation "com.squareup.retrofit2:converter-gson:$retrofit2_version"
implementation 'com.squareup.retrofit2:converter-protobuf:2.9.0' implementation "com.squareup.retrofit2:converter-protobuf:$retrofit2_version"
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation "com.squareup.retrofit2:retrofit:$retrofit2_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2"
implementation 'androidx.core:core-ktx:1.11.0-beta02' implementation 'androidx.core:core-ktx:1.11.0-beta02'
implementation 'androidx.leanback:leanback:1.2.0-alpha02' implementation 'androidx.leanback:leanback:1.2.0-alpha02'
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,112 @@
package com.lizongying.mytv
import android.graphics.Color
import android.util.Log
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
if (tvViewModel.logo.value != null) {
if (tvViewModel.title.value == "CCTV8K 超高清") {
Glide.with(viewHolder.view.context)
.load(R.drawable.cctv8k)
.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"
}
}
@@ -5,6 +5,7 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.Signature import android.content.pm.Signature
import android.content.pm.SigningInfo import android.content.pm.SigningInfo
import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
@@ -28,13 +29,16 @@ import java.security.MessageDigest
class MainActivity : FragmentActivity() { class MainActivity : FragmentActivity() {
var playerFragment = PlayerFragment() var playerFragment = PlayerFragment()
private val mainFragment = MainFragment() private val mainFragment = MainFragment2()
private val infoFragment = InfoFragment() private val infoFragment = InfoFragment()
private var doubleBackToExitPressedOnce = false private var doubleBackToExitPressedOnce = false
private lateinit var gestureDetector: GestureDetector private lateinit var gestureDetector: GestureDetector
private val handler = Handler()
private val delay: Long = 4000
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
@@ -49,6 +53,7 @@ class MainActivity : FragmentActivity() {
.add(R.id.main_browse_fragment, infoFragment) .add(R.id.main_browse_fragment, infoFragment)
.add(R.id.main_browse_fragment, mainFragment) .add(R.id.main_browse_fragment, mainFragment)
.hide(infoFragment) .hide(infoFragment)
.hide(mainFragment)
.commit() .commit()
mainFragment.view?.requestFocus() mainFragment.view?.requestFocus()
} }
@@ -60,7 +65,6 @@ class MainActivity : FragmentActivity() {
} }
fun play(tvViewModel: TVViewModel) { fun play(tvViewModel: TVViewModel) {
Log.i(TAG, "play: ${tvViewModel.getTV()}")
playerFragment.play(tvViewModel) playerFragment.play(tvViewModel)
mainFragment.view?.requestFocus() mainFragment.view?.requestFocus()
} }
@@ -74,18 +78,20 @@ class MainActivity : FragmentActivity() {
} }
private fun prevSource() { private fun prevSource() {
mainFragment.prevSource() // mainFragment.prevSource()
} }
private fun nextSource() { private fun nextSource() {
mainFragment.nextSource() // mainFragment.nextSource()
} }
fun switchMainFragment() { fun switchMainFragment() {
val transaction = supportFragmentManager.beginTransaction() val transaction = supportFragmentManager.beginTransaction()
if (mainFragment.isHidden) { if (mainFragment.isHidden) {
mainFragment.setPosition()
transaction.show(mainFragment) transaction.show(mainFragment)
keepRunnable()
} else { } else {
transaction.hide(mainFragment) transaction.hide(mainFragment)
} }
@@ -93,6 +99,15 @@ class MainActivity : FragmentActivity() {
transaction.commit() transaction.commit()
} }
fun keepRunnable() {
handler.removeCallbacks(hideRunnable)
handler.postDelayed(hideRunnable, delay)
}
private val hideRunnable = Runnable {
supportFragmentManager.beginTransaction().hide(mainFragment).commit()
}
private fun mainFragmentIsHidden(): Boolean { private fun mainFragmentIsHidden(): Boolean {
return mainFragment.isHidden return mainFragment.isHidden
} }
@@ -157,88 +172,132 @@ class MainActivity : FragmentActivity() {
} }
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { private fun showHelp() {
when (keyCode) { val versionName = getPackageInfo().versionName
KeyEvent.KEYCODE_BACK -> {
if (!mainFragmentIsHidden()) {
hideMainFragment()
return true
}
if (doubleBackToExitPressedOnce) { val textView = TextView(this)
super.onBackPressed() textView.text =
return true "当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/"
} textView.setBackgroundColor(0xFF263238.toInt())
textView.setPadding(20, 50, 20, 20)
this.doubleBackToExitPressedOnce = true val imageView = ImageView(this)
Toast.makeText(this, "再按一次退出", Toast.LENGTH_SHORT).show() val drawable = ContextCompat.getDrawable(this, R.drawable.appreciate)
imageView.setImageDrawable(drawable)
imageView.setBackgroundColor(Color.WHITE)
Handler(Looper.getMainLooper()).postDelayed({ val linearLayout = LinearLayout(this)
doubleBackToExitPressedOnce = false linearLayout.orientation = LinearLayout.VERTICAL
}, 2000) linearLayout.addView(textView)
return true linearLayout.addView(imageView)
}
KeyEvent.KEYCODE_MENU -> { val layoutParams = LinearLayout.LayoutParams(
val versionName = getPackageInfo().versionName LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
imageView.layoutParams = layoutParams
textView.layoutParams = layoutParams
val textView = TextView(this) val builder: AlertDialog.Builder = AlertDialog.Builder(this)
textView.text = builder
"当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/" .setView(linearLayout)
val imageView = ImageView(this) val dialog: AlertDialog = builder.create()
val drawable = ContextCompat.getDrawable(this, R.drawable.appreciate) dialog.show()
imageView.setImageDrawable(drawable) }
val linearLayout = LinearLayout(this) private fun channelUp() {
linearLayout.orientation = LinearLayout.VERTICAL if (mainFragment.isHidden) {
linearLayout.addView(textView) prev()
linearLayout.addView(imageView) } else {
val layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
imageView.layoutParams = layoutParams
textView.layoutParams = layoutParams
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder
.setView(linearLayout)
val dialog: AlertDialog = builder.create()
dialog.show()
return true
}
KeyEvent.KEYCODE_DPAD_CENTER -> {
Log.i(TAG, "KEYCODE_DPAD_CENTER")
switchMainFragment()
}
KeyEvent.KEYCODE_DPAD_UP -> {
if (mainFragment.isHidden) {
prev()
} else {
// if (mainFragment.selectedPosition == 0) { // if (mainFragment.selectedPosition == 0) {
// mainFragment.setSelectedPosition( // mainFragment.setSelectedPosition(
// mainFragment.tvListViewModel.maxNum.size - 1, // mainFragment.tvListViewModel.maxNum.size - 1,
// false // false
// ) // )
// } // }
} }
} }
KeyEvent.KEYCODE_DPAD_DOWN -> { private fun channelDown() {
if (mainFragment.isHidden) { if (mainFragment.isHidden) {
next() next()
} else { } else {
// if (mainFragment.selectedPosition == mainFragment.tvListViewModel.maxNum.size - 1) { // if (mainFragment.selectedPosition == mainFragment.tvListViewModel.maxNum.size - 1) {
//// mainFragment.setSelectedPosition(0, false) //// mainFragment.setSelectedPosition(0, false)
// hideMainFragment() // hideMainFragment()
// return false // return false
// } // }
} }
}
private fun back() {
if (!mainFragmentIsHidden()) {
hideMainFragment()
return
}
if (doubleBackToExitPressedOnce) {
super.onBackPressed()
return
}
doubleBackToExitPressedOnce = true
Toast.makeText(this, "再按一次退出", Toast.LENGTH_SHORT).show()
Handler(Looper.getMainLooper()).postDelayed({
doubleBackToExitPressedOnce = false
}, 2000)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
// Toast.makeText(this, "keyCode ${keyCodeToString(keyCode)}", Toast.LENGTH_SHORT).show()
when (keyCode) {
KeyEvent.KEYCODE_ESCAPE -> {
back()
return true
}
KeyEvent.KEYCODE_BACK -> {
back()
return true
}
KeyEvent.KEYCODE_UNKNOWN -> {
showHelp()
return true
}
KeyEvent.KEYCODE_HELP -> {
showHelp()
return true
}
KeyEvent.KEYCODE_SETTINGS -> {
showHelp()
return true
}
KeyEvent.KEYCODE_MENU -> {
showHelp()
return true
}
KeyEvent.KEYCODE_ENTER -> {
switchMainFragment()
}
KeyEvent.KEYCODE_DPAD_CENTER -> {
switchMainFragment()
}
KeyEvent.KEYCODE_DPAD_UP -> {
channelUp()
}
KeyEvent.KEYCODE_CHANNEL_UP -> {
channelUp()
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
channelDown()
}
KeyEvent.KEYCODE_CHANNEL_DOWN -> {
channelDown()
} }
KeyEvent.KEYCODE_DPAD_LEFT -> { KeyEvent.KEYCODE_DPAD_LEFT -> {
@@ -6,6 +6,7 @@ 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.widget.Toast
import androidx.leanback.app.BrowseSupportFragment import androidx.leanback.app.BrowseSupportFragment
import androidx.leanback.widget.ArrayObjectAdapter import androidx.leanback.widget.ArrayObjectAdapter
import androidx.leanback.widget.HeaderItem import androidx.leanback.widget.HeaderItem
@@ -63,6 +64,13 @@ class MainFragment : BrowseSupportFragment() {
handler.post(mUpdateProgramRunnable) handler.post(mUpdateProgramRunnable)
tvListViewModel.getTVListViewModel().value?.forEach { tvViewModel -> tvListViewModel.getTVListViewModel().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) { _ -> tvViewModel.ready.observe(viewLifecycleOwner) { _ ->
// not first time && channel not change // not first time && channel not change
@@ -78,8 +86,8 @@ class MainFragment : BrowseSupportFragment() {
if (tvViewModel.change.value != null) { if (tvViewModel.change.value != null) {
val title = tvViewModel.title.value val title = tvViewModel.title.value
Log.i(TAG, "switch $title") Log.i(TAG, "switch $title")
if (tvViewModel.pid.value != null) { if (tvViewModel.pid.value != "") {
Log.i(TAG, "request $title") Log.i(TAG, "request $title ${tvViewModel.pid.value}")
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
tvViewModel.let { request.fetchData(it) } tvViewModel.let { request.fetchData(it) }
} }
@@ -201,8 +209,8 @@ class MainFragment : BrowseSupportFragment() {
itemPosition = sharedPref?.getInt("position", 0)!! itemPosition = sharedPref?.getInt("position", 0)!!
if (itemPosition >= tvListViewModel.size()) { if (itemPosition >= tvListViewModel.size()) {
itemPosition = 0 itemPosition = 0
tvListViewModel.setItemPosition(itemPosition)
} }
tvListViewModel.setItemPosition(itemPosition)
} }
fun fragmentReady() { fun fragmentReady() {
@@ -305,6 +313,7 @@ class MainFragment : BrowseSupportFragment() {
) { ) {
if (item is TVViewModel) { if (item is TVViewModel) {
tvListViewModel.setItemPositionCurrent(item.id.value!!) tvListViewModel.setItemPositionCurrent(item.id.value!!)
(activity as MainActivity).keepRunnable()
} }
} }
} }
@@ -0,0 +1,288 @@
package com.lizongying.mytv
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.lizongying.mytv.Utils.dpToPx
import com.lizongying.mytv.databinding.RowBinding
import com.lizongying.mytv.databinding.ShowBinding
import com.lizongying.mytv.models.TVListViewModel
import com.lizongying.mytv.models.TVViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainFragment2 : Fragment(), CardAdapter.ItemListener {
private var itemPosition = 0
private var rowList: MutableList<View> = mutableListOf()
private var _binding: ShowBinding? = null
private val binding get() = _binding!!
private var request: Request = Request()
var tvListViewModel = TVListViewModel()
private var sharedPref: SharedPreferences? = null
private var lastVideoUrl: String = ""
private val handler = Handler(Looper.getMainLooper())
private lateinit var mUpdateProgramRunnable: UpdateProgramRunnable
private var ready = 0
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = ShowBinding.inflate(inflater, container, false)
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
activity?.let { request.initYSP(it) }
sharedPref = activity?.getPreferences(Context.MODE_PRIVATE)
view?.post {
val content = binding.content
var idx: Long = 0
for ((k, v) in TVList.list) {
val itemBinding: RowBinding =
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)
}
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++
}
mUpdateProgramRunnable = UpdateProgramRunnable()
handler.post(mUpdateProgramRunnable)
itemPosition = sharedPref?.getInt("position", 0)!!
if (itemPosition >= tvListViewModel.size()) {
itemPosition = 0
}
setPosition()
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVListViewModel().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 ${tvViewModel.pid.value}")
lifecycleScope.launch(Dispatchers.IO) {
tvViewModel.let { request.fetchData(it) }
}
(activity as? MainActivity)?.showInfoFragment(tvViewModel)
setPosition(
tvViewModel.getRowPosition(), tvViewModel.getItemPosition()
)
} else {
if (check(tvViewModel)) {
(activity as? MainActivity)?.play(tvViewModel)
(activity as? MainActivity)?.showInfoFragment(tvViewModel)
setPosition(
tvViewModel.getRowPosition(), tvViewModel.getItemPosition()
)
}
}
}
}
}
fragmentReady()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
override fun onItemFocusChange(tvViewModel: TVViewModel, hasFocus: Boolean) {
if (hasFocus) {
tvListViewModel.setItemPositionCurrent(tvViewModel.id.value!!)
(activity as MainActivity).keepRunnable()
}
}
override fun onItemClicked(tvViewModel: TVViewModel) {
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 LinearLayoutManager).findViewByPosition(
tvViewModel.getItemPosition()
)?.requestFocus()
}
}
fun setPosition(rowPosition: Int, itemPosition: Int) {
rowList[rowPosition].post {
((rowList[rowPosition] as RecyclerView).layoutManager as LinearLayoutManager).findViewByPosition(
itemPosition
)?.requestFocus()
}
}
fun check(tvViewModel: TVViewModel): Boolean {
val title = tvViewModel.title.value
val videoUrl = tvViewModel.videoIndex.value?.let { tvViewModel.videoUrl.value?.get(it) }
if (videoUrl == null || videoUrl == "") {
Log.e(TAG, "$title videoUrl is empty")
return false
}
if (videoUrl == lastVideoUrl) {
Log.e(TAG, "$title videoUrl is duplication")
return false
}
return true
}
fun fragmentReady() {
ready++
Log.i(TAG, "ready $ready")
if (ready == 3) {
// request.fetchPage()
tvListViewModel.getTVViewModel(itemPosition)?.changed()
}
}
fun prev() {
view?.post {
itemPosition--
if (itemPosition == -1) {
itemPosition = tvListViewModel.size() - 1
}
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVViewModel(itemPosition)?.changed()
}
}
fun next() {
view?.post {
itemPosition++
if (itemPosition == tvListViewModel.size()) {
itemPosition = 0
}
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVViewModel(itemPosition)?.changed()
}
}
fun updateProgram(tvViewModel: TVViewModel) {
val timestamp = Utils.getDateTimestamp()
if (timestamp - tvViewModel.programUpdateTime > 60) {
if (tvViewModel.program.value!!.isEmpty()) {
tvViewModel.programUpdateTime = timestamp
request.fetchProgram(tvViewModel)
} else {
if (timestamp - tvViewModel.program.value!!.last().et < 600) {
tvViewModel.programUpdateTime = timestamp
request.fetchProgram(tvViewModel)
}
}
}
}
inner class UpdateProgramRunnable : Runnable {
override fun run() {
tvListViewModel.getTVListViewModel().value?.filter { it.programId.value != null }
?.forEach { tvViewModel ->
updateProgram(
tvViewModel
)
}
handler.postDelayed(this, 60000)
}
}
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacks(mUpdateProgramRunnable)
with(sharedPref!!.edit()) {
putInt("position", itemPosition)
apply()
}
}
override fun onResume() {
super.onResume()
view!!.requestFocus()
}
companion object {
private const val TAG = "MainFragment"
}
}
@@ -24,6 +24,7 @@ class PlayerFragment : Fragment() {
private var _binding: PlayerBinding? = null private var _binding: PlayerBinding? = null
private var playerView: PlayerView? = null private var playerView: PlayerView? = null
private var tvViewModel: TVViewModel? = null private var tvViewModel: TVViewModel? = null
private val aspectRatio = 16f / 9f
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
@@ -43,10 +44,9 @@ class PlayerFragment : Fragment() {
playerView!!.player?.playWhenReady = true playerView!!.player?.playWhenReady = true
playerView!!.player?.addListener(object : Player.Listener { playerView!!.player?.addListener(object : Player.Listener {
override fun onVideoSizeChanged(videoSize: VideoSize) { override fun onVideoSizeChanged(videoSize: VideoSize) {
val aspectRatio = 16f / 9f
val layoutParams = playerView?.layoutParams
val ratio = playerView?.measuredWidth?.div(playerView?.measuredHeight!!) val ratio = playerView?.measuredWidth?.div(playerView?.measuredHeight!!)
if (ratio != null) { if (ratio != null) {
val layoutParams = playerView?.layoutParams
if (ratio < aspectRatio) { if (ratio < aspectRatio) {
layoutParams?.height = layoutParams?.height =
(playerView?.measuredWidth?.div(aspectRatio))?.toInt() (playerView?.measuredWidth?.div(aspectRatio))?.toInt()
@@ -7,11 +7,9 @@ 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.BtraceClient
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
import com.lizongying.mytv.api.ProtoClient
import com.lizongying.mytv.api.YSP import com.lizongying.mytv.api.YSP
import com.lizongying.mytv.api.YSPApiService import com.lizongying.mytv.api.YSPApiService
import com.lizongying.mytv.api.YSPBtraceService import com.lizongying.mytv.api.YSPBtraceService
@@ -31,8 +29,8 @@ import javax.crypto.spec.SecretKeySpec
class Request { class Request {
private var yspTokenService: YSPTokenService = ApiClient().yspTokenService private var yspTokenService: YSPTokenService = ApiClient().yspTokenService
private var yspApiService: YSPApiService = ApiClient().yspApiService private var yspApiService: YSPApiService = ApiClient().yspApiService
private var yspBtraceService: YSPBtraceService = BtraceClient().yspBtraceService private var yspBtraceService: YSPBtraceService = ApiClient().yspBtraceService
private var yspProtoService: YSPProtoService = ProtoClient().yspProtoService private var yspProtoService: YSPProtoService = ApiClient().yspProtoService
private var ysp: YSP? = null private var ysp: YSP? = null
// TODO onDestroy // TODO onDestroy
@@ -62,7 +60,6 @@ class Request {
"CGTN阿拉伯语频道" to "CGTN 阿拉伯语频道", "CGTN阿拉伯语频道" to "CGTN 阿拉伯语频道",
"CGTN西班牙语频道" to "CGTN 西班牙语频道", "CGTN西班牙语频道" to "CGTN 西班牙语频道",
"CGTN外语纪录频道" to "CGTN 纪录频道", "CGTN外语纪录频道" to "CGTN 纪录频道",
"东方卫视" to "东方卫视", "东方卫视" to "东方卫视",
"湖南卫视" to "湖南卫视", "湖南卫视" to "湖南卫视",
"湖北卫视" to "湖北卫视", "湖北卫视" to "湖北卫视",
@@ -86,13 +83,14 @@ class Request {
"海南卫视" to "海南卫视", "海南卫视" to "海南卫视",
) )
private var token: String? = null
fun initYSP(context: Context) { fun initYSP(context: Context) {
ysp = YSP(context) ysp = YSP(context)
} }
var call: Call<LiveInfo>? = null
fun fetchVideo(tvModel: TVViewModel, cookie: String) { fun fetchVideo(tvModel: TVViewModel, cookie: String) {
call?.cancel()
if (::myRunnable.isInitialized) { if (::myRunnable.isInitialized) {
handler.removeCallbacks(myRunnable) handler.removeCallbacks(myRunnable)
} }
@@ -102,117 +100,107 @@ 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", it) }
request?.let { yspApiService.getLiveInfo(cookie, it) } call?.enqueue(object : Callback<LiveInfo> {
?.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( chanll.substring(9, chanll.length - 3),
chanll.substring(9, chanll.length - 3), Base64.DEFAULT
Base64.DEFAULT )
) val decodedString = String(decodedBytes)
val decodedString = String(decodedBytes) val regex = Regex("""des_key = "([^"]+).+var des_iv = "([^"]+)""")
val regex = Regex("""des_key = "([^"]+).+var des_iv = "([^"]+)""") val matchResult = regex.find(decodedString)
val matchResult = regex.find(decodedString) if (matchResult != null) {
if (matchResult != null) { val (key, iv) = matchResult.destructured
val (key, iv) = matchResult.destructured val keyBytes = Base64.decode(key, Base64.DEFAULT)
val keyBytes = Base64.decode(key, Base64.DEFAULT) val ivBytes = Base64.decode(iv, Base64.DEFAULT)
val ivBytes = Base64.decode(iv, Base64.DEFAULT) 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()
).uppercase() Log.i(TAG, "$title url $url")
Log.i(TAG, "$title url $url") tvModel.addVideoUrl(url)
tvModel.addVideoUrl(url) tvModel.allReady()
tvModel.allReady() tvModel.retryTimes = 0
tvModel.retryTimes = 0 myRunnable = MyRunnable(tvModel)
myRunnable = MyRunnable(tvModel) handler.post(myRunnable)
handler.post(myRunnable)
} else {
Log.e(TAG, "$title key error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchData(tvModel)
}
}
} else { } else {
Log.e(TAG, "$title url error $request") Log.e(TAG, "$title key error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) { if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++ tvModel.retryTimes++
fetchData(tvModel) fetchVideo(tvModel, cookie)
} }
} }
} else { } else {
Log.e(TAG, "$title status error") if (liveInfo?.data?.errinfo != null && liveInfo.data.errinfo == "应版权方要求,暂停提供直播信号,请点击观看其他精彩节目") {
Log.e(TAG, "$title error ${liveInfo.data.errinfo}")
tvModel.setErrInfo(liveInfo.data.errinfo)
} else {
Log.e(TAG, "$title url error $request $liveInfo")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
}
}
} else {
Log.e(TAG, "$title status error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
}
}
override fun onFailure(call: Call<LiveInfo>, t: Throwable) {
Log.e(TAG, "$title request error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
}
})
}
fun fetchVideo(tvModel: TVViewModel) {
yspTokenService.getInfo()
.enqueue(object : Callback<Info> {
override fun onResponse(call: Call<Info>, response: Response<Info>) {
if (response.isSuccessful) {
val token = response.body()?.data?.token
Log.i(TAG, "info success $token")
val cookie =
"vplatform=109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchVideo(tvModel, cookie)
} else {
Log.e(TAG, "info status error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) { if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++ tvModel.retryTimes++
fetchData(tvModel) fetchVideo(tvModel)
} }
} }
} }
override fun onFailure(call: Call<LiveInfo>, t: Throwable) { override fun onFailure(call: Call<Info>, t: Throwable) {
Log.e(TAG, "$title request error") Log.e(TAG, "info request error $t")
if (tvModel.retryTimes < tvModel.retryMaxTimes) { if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++ tvModel.retryTimes++
fetchData(tvModel) fetchVideo(tvModel)
} }
} }
}) })
} }
fun fetchData(tvModel: TVViewModel) { fun fetchData(tvModel: TVViewModel) {
var cookie = "guid=1; vplatform=109" if (tvModel.needToken) {
val channels = arrayOf( fetchVideo(tvModel)
"CCTV3 综艺",
"CCTV6 电影",
"CCTV8 电视剧",
"风云剧场",
"第一剧场",
"怀旧剧场",
"世界地理",
"风云音乐",
"兵器科技",
"风云足球",
"高尔夫网球",
"女性时尚",
"央视文化精品",
"央视台球",
"电视指南",
"卫生健康",
)
if (tvModel.title.value in channels) {
yspTokenService.getInfo()
.enqueue(object : Callback<Info> {
override fun onResponse(call: Call<Info>, response: Response<Info>) {
if (response.isSuccessful) {
val info = response.body()
token = info?.data?.token
Log.i(TAG, "info success $token")
cookie =
"guid=1; vplatform=109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchVideo(tvModel, cookie)
} else {
Log.e(TAG, "info status error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchData(tvModel)
}
}
}
override fun onFailure(call: Call<Info>, t: Throwable) {
Log.e(TAG, "info request error $t")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchData(tvModel)
}
}
})
} else { } else {
val cookie = "vplatform=109"
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
} }
} }
@@ -1,5 +1,7 @@
package com.lizongying.mytv package com.lizongying.mytv
import android.content.res.Resources
import android.util.TypedValue
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@@ -12,4 +14,20 @@ object Utils {
fun getDateTimestamp(): Long { fun getDateTimestamp(): Long {
return Date().time / 1000 return Date().time / 1000
} }
fun dpToPx(dp: Float): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp,
Resources.getSystem().displayMetrics
).toInt()
}
fun dpToPx(dp: Int): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp.toFloat(),
Resources.getSystem().displayMetrics
).toInt()
}
} }
@@ -1,18 +1,27 @@
package com.lizongying.mytv.api package com.lizongying.mytv.api
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 javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
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 myUrl = "http://10.0.2.2:8081/" private val protoUrl = "https://capi.yangshipin.cn/"
private val traceUrl = "https://btrace.yangshipin.cn/"
private var okHttpClient = getUnsafeOkHttpClient()
val yspApiService: YSPApiService by lazy { val yspApiService: YSPApiService by lazy {
Retrofit.Builder() Retrofit.Builder()
.baseUrl(yspUrl) .baseUrl(yspUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build().create(YSPApiService::class.java) .build().create(YSPApiService::class.java)
} }
@@ -20,7 +29,60 @@ class ApiClient {
val yspTokenService: YSPTokenService by lazy { val yspTokenService: YSPTokenService by lazy {
Retrofit.Builder() Retrofit.Builder()
.baseUrl(myUrl) .baseUrl(myUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build().create(YSPTokenService::class.java) .build().create(YSPTokenService::class.java)
} }
val yspProtoService: YSPProtoService by lazy {
Retrofit.Builder()
.baseUrl(protoUrl)
.client(okHttpClient)
.addConverterFactory(ProtoConverterFactory.create())
.build().create(YSPProtoService::class.java)
}
val yspBtraceService: YSPBtraceService by lazy {
Retrofit.Builder()
.baseUrl(traceUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build().create(YSPBtraceService::class.java)
}
private fun getUnsafeOkHttpClient(): OkHttpClient {
try {
val trustAllCerts: Array<TrustManager> = arrayOf(
object : X509TrustManager {
override fun checkClientTrusted(
chain: Array<out java.security.cert.X509Certificate>?,
authType: String?
) {
}
override fun checkServerTrusted(
chain: Array<out java.security.cert.X509Certificate>?,
authType: String?
) {
}
override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> {
return emptyArray()
}
}
)
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
return OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager)
.hostnameVerifier { _, _ -> true }
.dns(DnsCache())
.build()
} catch (e: Exception) {
throw RuntimeException(e)
}
}
} }
@@ -1,17 +0,0 @@
package com.lizongying.mytv.api
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class BtraceClient {
private val yspUrl = "https://btrace.yangshipin.cn/"
val yspBtraceService: YSPBtraceService by lazy {
Retrofit.Builder()
.baseUrl(yspUrl)
.addConverterFactory(GsonConverterFactory.create())
.build().create(YSPBtraceService::class.java)
}
}
@@ -0,0 +1,23 @@
package com.lizongying.mytv.api
import okhttp3.Dns
import java.net.InetAddress
import java.util.concurrent.ConcurrentHashMap
class DnsCache : Dns {
private val dnsCache: MutableMap<String, List<InetAddress>> = ConcurrentHashMap()
override fun lookup(hostname: String): List<InetAddress> {
dnsCache[hostname]?.let {
return it
}
val addresses = InetAddress.getAllByName(hostname).toList()
if (addresses.isNotEmpty()) {
dnsCache[hostname] = addresses
}
return addresses
}
}
@@ -15,6 +15,7 @@ data class LiveInfo(
data class LiveInfoData( data class LiveInfoData(
val chanll: String, val chanll: String,
val playurl: String, val playurl: String,
val errinfo: String,
) )
data class LiveInfoRequest( data class LiveInfoRequest(
@@ -1,16 +0,0 @@
package com.lizongying.mytv.api
import retrofit2.Retrofit
import retrofit2.converter.protobuf.ProtoConverterFactory
class ProtoClient {
private val protoUrl = "https://capi.yangshipin.cn/"
val yspProtoService: YSPProtoService by lazy {
Retrofit.Builder()
.baseUrl(protoUrl)
.addConverterFactory(ProtoConverterFactory.create())
.build().create(YSPProtoService::class.java)
}
}
@@ -70,6 +70,11 @@ class YSP(var context: Context) {
defn = "fhd" defn = "fhd"
randStr = getRand() randStr = getRand()
if (tvModel.retryTimes > 0) {
guid = newGuid()
}
timeStr = getTimeStr() timeStr = getTimeStr()
// guid = "lq3oqitm_1e15dnzgjnb" // guid = "lq3oqitm_1e15dnzgjnb"
@@ -105,6 +110,15 @@ class YSP(var context: Context) {
return guid return guid
} }
private fun newGuid(): String {
guid = generateGuid()
with(sharedPref!!.edit()) {
putString("guid", guid)
apply()
}
return guid
}
fun getRand(): String { fun getRand(): String {
var n = "" var n = ""
val e = "ABCDEFGHIJKlMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" val e = "ABCDEFGHIJKlMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
@@ -1,6 +1,5 @@
package com.lizongying.mytv.models package com.lizongying.mytv.models
import android.content.SharedPreferences
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -20,9 +20,13 @@ class TVViewModel(private var tv: TV) : ViewModel() {
private var itemPosition: Int = 0 private var itemPosition: Int = 0
var retryTimes: Int = 0 var retryTimes: Int = 0
var retryMaxTimes: Int = 3 var retryMaxTimes: Int = 5
var programUpdateTime: Long = 0 var programUpdateTime: Long = 0
private val _errInfo = MutableLiveData<String>()
val errInfo: LiveData<String>
get() = _errInfo
private val _programId = MutableLiveData<String>() private val _programId = MutableLiveData<String>()
val programId: LiveData<String> val programId: LiveData<String>
get() = _programId get() = _programId
@@ -67,10 +71,31 @@ class TVViewModel(private var tv: TV) : ViewModel() {
val ready: LiveData<Boolean> val ready: LiveData<Boolean>
get() = _ready get() = _ready
private var mMinimumLoadableRetryCount = 3 private var mMinimumLoadableRetryCount = 5
var seq = 0 var seq = 0
var needToken = false
private val channelsNeedToken = arrayOf(
"CCTV3 综艺",
"CCTV6 电影",
"CCTV8 电视剧",
"风云剧场",
"第一剧场",
"怀旧剧场",
"世界地理",
"风云音乐",
"兵器科技",
"风云足球",
"高尔夫网球",
"女性时尚",
"央视文化精品",
"央视台球",
"电视指南",
"卫生健康",
)
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")) {
@@ -121,6 +146,10 @@ class TVViewModel(private var tv: TV) : ViewModel() {
_pid.value = tv.pid _pid.value = tv.pid
_sid.value = tv.sid _sid.value = tv.sid
_program.value = mutableListOf() _program.value = mutableListOf()
if (tv.title in channelsNeedToken) {
needToken = true
}
} }
fun getRowPosition(): Int { fun getRowPosition(): Int {
@@ -139,6 +168,10 @@ class TVViewModel(private var tv: TV) : ViewModel() {
itemPosition = position itemPosition = position
} }
fun setErrInfo(info: String) {
_errInfo.value = info
}
fun update(t: TV) { fun update(t: TV) {
tv = t tv = t
} }
+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="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
@@ -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>
+16 -2
View File
@@ -1,11 +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">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>