Compare commits

..
15 Commits
Author SHA1 Message Date
Li ZongYing aa4d52681f fix ysp 2024-02-07 03:05:11 +08:00
Li ZongYing 8137515605 fix ysp err 2024-02-05 06:25:31 +08:00
Li ZongYing 6c16408b90 upgrade player 2024-02-04 22:23:28 +08:00
Li ZongYing 44ab3a6669 fix channel number did not disappear 2024-02-03 23:30:37 +08:00
Li ZongYing 917b245bbe readme 2024-02-02 23:34:39 +08:00
Li ZongYing dd4550cf4f replay on resume 2024-02-01 18:10:58 +08:00
Li ZongYing 4bdc625718 add channel 2024-01-26 18:30:31 +08:00
Li ZongYing cda19995b2 optimize channel switch 2024-01-25 15:12:47 +08:00
Li ZongYing 1eb2f257b5 change channel order 2024-01-25 11:04:06 +08:00
Li ZongYing 9feb0dc4bc channel num setting 2024-01-19 12:18:17 +08:00
Li ZongYing 46311036d6 number selection 2024-01-17 16:27:25 +08:00
Li ZongYing 1aade3a48d 17ok 2024-01-14 18:58:37 +08:00
Li ZongYing 2528cf33db test 17 2024-01-14 10:09:59 +08:00
Li ZongYing 4364f66ec5 17 2024-01-14 02:53:09 +08:00
Li ZongYing 49cd7b3af2 help page 2024-01-11 16:32:08 +08:00
17 changed files with 367 additions and 445 deletions
+1 -1
View File
@@ -112,4 +112,4 @@ adb install my-tv.apk
## 赞赏
![image](./screenshots/zfb.jpg)
![image](./screenshots/appreciate.jpeg)
+1 -1
View File
@@ -101,7 +101,7 @@ dependencies {
implementation "androidx.media3:media3-exoplayer-hls:$media3_version"
// 21:2.9.0 17:2.6.4
def retrofit2_version = "2.9.0"
def retrofit2_version = "2.6.4"
implementation 'com.google.protobuf:protobuf-kotlin:3.25.1'
implementation "com.squareup.retrofit2:converter-gson:$retrofit2_version"
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,119 @@
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)
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"
}
}
@@ -1,157 +0,0 @@
package com.lizongying.mytv
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
/**
*@author LeGend
*@date 2024/2/4 22:42
*/
object ChannelUtils {
/**
* 获取服务器channel版本
*
* @param context Context
*
* @return 服务器channel版本
*/
suspend fun getServerVersion(context: Context): Int {
return withContext(Dispatchers.IO) {
val client = okhttp3.OkHttpClient.Builder().connectTimeout(500, java.util.concurrent.TimeUnit.MILLISECONDS)
.readTimeout(1, java.util.concurrent.TimeUnit.SECONDS).build()
client.newCall(okhttp3.Request.Builder().url(getServerVersionUrl(context)).build()).execute()
.use { response ->
if (!response.isSuccessful) throw java.io.IOException("Unexpected code $response")
val body = response.body()
body?.string()?.toInt() ?: 0
}
}
}
/**
* 获取服务器channel
*
* @param url String 服务器地址
*
* @return Array<TV> 服务器channel
*
* @throws java.io.IOException 网络请求失败
*/
suspend fun getServerChannel(url: String): List<TV> {
val result = withContext(Dispatchers.IO) {
val client = okhttp3.OkHttpClient.Builder().connectTimeout(500, java.util.concurrent.TimeUnit.MILLISECONDS)
.readTimeout(1, java.util.concurrent.TimeUnit.SECONDS).build()
val request = okhttp3.Request.Builder().url(url).build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw java.io.IOException("Unexpected code $response")
val body = response.body()
body?.string() ?: ""
}
}
return withContext(Dispatchers.Default) {
val type = object : com.google.gson.reflect.TypeToken<List<TV>>() {}.type
com.google.gson.Gson().fromJson(result, type)
}
}
/**
* 获取服务器地址
*
* @param context Context
*
* @return 服务器地址
*/
fun getServerUrl(context: Context): String {
return context.resources.getString(R.string.server_url)
}
/**
* 获取serverVersion的URL
*
* @param context Context
*
* @return serverVersionURL 服务器版本地址
*/
suspend fun getServerVersionUrl(context: Context): String {
return withContext(Dispatchers.IO) {
context.resources.getString(R.string.server_version_url)
}
}
/**
* 获取本地channel版本
*
* @param context Context
*
* @return 本地channel
*/
suspend fun getLocalVersion(context: Context): Int {
return withContext(Dispatchers.IO) {
val file = File(getAppDirectory(context), "channels")
//检查本地是否已经有保存的channels.json,若无保存的Channel.json则从读取assert中文件
val savedVersion =
context.getSharedPreferences("saved_version", Context.MODE_PRIVATE).getInt("version", Integer.MIN_VALUE)
if (!file.exists() || savedVersion == Integer.MIN_VALUE) {
context.resources.getInteger(R.integer.local_channel_version)
} else {
savedVersion
}
}
}
/**
* 获取本地可读取的目录
* @param context Context
*
* @return 可读取的目录
*/
private fun getAppDirectory(context: Context): File {
return context.filesDir
}
/**
* 获取本地channel
*
* @param context Context
*
* @return Array<TV> 本地channel
*/
suspend fun getLocalChannel(context: Context): List<TV> {
val str = withContext(Dispatchers.IO) {
if (File(getAppDirectory(context), "channels").exists()) {
File(getAppDirectory(context), "channels").readText()
} else {
context.resources.openRawResource(R.raw.channels).bufferedReader().use { it.readText() }
}
}
return withContext(Dispatchers.Default) {
val type = object : com.google.gson.reflect.TypeToken<List<TV>>() {}.type
com.google.gson.Gson().fromJson(str, type)
}
}
/**
* 更新channels.json
*
* @param context Context
*
* @return 无
*
* @throws java.io.IOException 写入失败
*/
suspend fun updateLocalChannel(context: Context, version: Int, channels: List<TV>) {
withContext(Dispatchers.IO) {
val file = File(getAppDirectory(context), "channels")
if (!file.exists()) {
file.createNewFile()
}
file.writeText(com.google.gson.Gson().toJson(channels))
context.getSharedPreferences("saved_version", Context.MODE_PRIVATE).edit().putInt(
"version", version
).apply()
}
}
}
@@ -122,6 +122,7 @@ class MainActivity : FragmentActivity() {
val transaction = supportFragmentManager.beginTransaction()
if (mainFragment.isHidden) {
mainFragment.setPosition()
transaction.show(mainFragment)
keepRunnable()
} else {
@@ -5,30 +5,31 @@ 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.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.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.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.TVViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainFragment : BrowseSupportFragment() {
class MainFragment : Fragment(), CardAdapter.ItemListener {
private var itemPosition = 0
private var rowsAdapter: ArrayObjectAdapter? = null
private var rowList: MutableList<View> = mutableListOf()
private var _binding: ShowBinding? = null
private val binding get() = _binding!!
private var request = Request()
@@ -43,15 +44,12 @@ class MainFragment : BrowseSupportFragment() {
private var ready = 0
override fun onCreate(savedInstanceState: Bundle?) {
Log.i(TAG, "onCreate")
super.onCreate(savedInstanceState)
headersState = HEADERS_DISABLED
}
override fun onStart() {
Log.i(TAG, "onStart")
super.onStart()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = ShowBinding.inflate(inflater, container, false)
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
@@ -60,171 +58,140 @@ class MainFragment : BrowseSupportFragment() {
activity?.let { request.initYSP(it) }
sharedPref = (activity as? MainActivity)?.sharedPref!!
loadRows()
view?.post {
val content = binding.content
setupEventListeners()
var idx: Long = 0
for ((k, v) in TVList.list) {
val itemBinding: RowBinding =
RowBinding.inflate(layoutInflater, content, false)
mUpdateProgramRunnable = UpdateProgramRunnable()
handler.post(mUpdateProgramRunnable)
tvListViewModel.tvListViewModel.value?.forEach { tvViewModel ->
tvViewModel.errInfo.observe(viewLifecycleOwner) { _ ->
if (tvViewModel.errInfo.value != null
&& tvViewModel.id.value == itemPosition
) {
Toast.makeText(context, tvViewModel.errInfo.value, Toast.LENGTH_SHORT).show()
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)
}
}
tvViewModel.ready.observe(viewLifecycleOwner) { _ ->
tvListViewModel.maxNum.add(v.size)
// 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)
}
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.change.observe(viewLifecycleOwner) { _ ->
if (tvViewModel.change.value != null) {
val title = tvViewModel.title.value
if (tvViewModel.pid.value != "") {
Log.i(TAG, "request $title")
lifecycleScope.launch(Dispatchers.IO) {
tvViewModel.let { request.fetchData(it) }
}
(activity as? MainActivity)?.showInfoFragment(tvViewModel)
setSelectedPosition(
tvViewModel.getRowPosition(), true,
SelectItemViewHolderTask(tvViewModel.getItemPosition())
)
} else {
if (check(tvViewModel)) {
(activity as? MainActivity)?.play(tvViewModel)
mUpdateProgramRunnable = UpdateProgramRunnable()
handler.post(mUpdateProgramRunnable)
itemPosition = sharedPref.getInt(POSITION, 0)
if (itemPosition >= tvListViewModel.size()) {
itemPosition = 0
}
tvListViewModel.setItemPosition(itemPosition)
setPosition()
tvListViewModel.tvListViewModel.value?.forEach { tvViewModel ->
tvViewModel.errInfo.observe(viewLifecycleOwner) { _ ->
if (tvViewModel.errInfo.value != null
&& tvViewModel.id.value == itemPosition
) {
Toast.makeText(context, tvViewModel.errInfo.value, Toast.LENGTH_SHORT)
.show()
}
}
tvViewModel.ready.observe(viewLifecycleOwner) { _ ->
// not first time && channel not change
if (tvViewModel.ready.value != null
&& tvViewModel.id.value == itemPosition
&& check(tvViewModel)
) {
Log.i(TAG, "ready ${tvViewModel.title.value}")
(activity as? MainActivity)?.play(tvViewModel)
}
}
tvViewModel.change.observe(viewLifecycleOwner) { _ ->
if (tvViewModel.change.value != null) {
val title = tvViewModel.title.value
Log.i(TAG, "switch $title")
if (tvViewModel.pid.value != "") {
Log.i(TAG, "request $title")
lifecycleScope.launch(Dispatchers.IO) {
tvViewModel.let { request.fetchData(it) }
}
(activity as? MainActivity)?.showInfoFragment(tvViewModel)
setSelectedPosition(
tvViewModel.getRowPosition(), true,
SelectItemViewHolderTask(tvViewModel.getItemPosition())
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()
}
fun toLastPosition() {
setSelectedPosition(
selectedPosition, false,
SelectItemViewHolderTask(tvListViewModel.maxNum[selectedPosition] - 1)
)
}
fun toFirstPosition() {
setSelectedPosition(
selectedPosition, false,
SelectItemViewHolderTask(0)
)
}
override fun startHeadersTransition(withHeaders: Boolean) {
}
private fun loadRows() {
rowsAdapter = ArrayObjectAdapter(ListRowPresenter())
val cardPresenter = CardPresenter(viewLifecycleOwner)
var idx: Long = 0
for ((k, v) in TVList.list) {
val listRowAdapter = ArrayObjectAdapter(cardPresenter)
for ((idx2, v1) in v.withIndex()) {
val tvViewModel = TVViewModel(v1)
tvViewModel.setRowPosition(idx.toInt())
tvViewModel.setItemPosition(idx2)
tvListViewModel.addTVViewModel(tvViewModel)
listRowAdapter.add(tvViewModel)
}
tvListViewModel.maxNum.add(v.size)
val header = HeaderItem(idx, k)
rowsAdapter!!.add(ListRow(header, listRowAdapter))
idx++
}
adapter = rowsAdapter
itemPosition = 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()
}
}
fragmentReady()
}
}
fun nextSource() {
view?.post {
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
if (tvViewModel != null) {
if (tvViewModel.videoUrl.value!!.size > 1) {
val videoIndex = tvViewModel.videoIndex.value?.plus(1)
if (videoIndex == tvViewModel.videoUrl.value!!.size) {
tvViewModel.setVideoIndex(0)
}
tvViewModel.changed()
}
}
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()
}
}
private fun setupEventListeners() {
onItemViewClickedListener = ItemViewClickedListener()
onItemViewSelectedListener = ItemViewSelectedListener()
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()
}
private inner class ItemViewClickedListener : OnItemViewClickedListener {
override fun onItemClicked(
itemViewHolder: Presenter.ViewHolder,
item: Any,
rowViewHolder: RowPresenter.ViewHolder,
row: Row
) {
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() {
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
rowList[tvViewModel!!.getRowPosition()].post {
((rowList[tvViewModel.getRowPosition()] as RecyclerView).layoutManager as LinearLayoutManager).findViewByPosition(
tvViewModel.getItemPosition()
)?.requestFocus()
}
}
private inner class ItemViewSelectedListener : OnItemViewSelectedListener {
override fun onItemSelected(
itemViewHolder: Presenter.ViewHolder?, item: Any?,
rowViewHolder: RowPresenter.ViewHolder, row: Row
) {
if (item is TVViewModel) {
tvListViewModel.setItemPositionCurrent(item.id.value!!)
(activity as MainActivity).keepRunnable()
}
fun setPosition(rowPosition: Int, itemPosition: Int) {
rowList[rowPosition].post {
((rowList[rowPosition] as RecyclerView).layoutManager as LinearLayoutManager).findViewByPosition(
itemPosition
)?.requestFocus()
}
}
@@ -335,6 +302,12 @@ class MainFragment : BrowseSupportFragment() {
handler.removeCallbacks(mUpdateProgramRunnable)
}
override fun onDestroyView() {
Log.i(TAG, "onDestroyView")
super.onDestroyView()
_binding = null
}
companion object {
private const val TAG = "MainFragment"
private const val POSITION = "position"
@@ -1,12 +1,5 @@
package com.lizongying.mytv
import android.content.Context
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.IOException
object TVList {
var list = mapOf(
"央视频道" to listOf(
@@ -950,63 +943,4 @@ object TVList {
),
)
)
/**
* 初始化
*
* @param context Context
*/
fun init(context: Context) {
CoroutineScope(Dispatchers.Default).launch {
//获取本地版本号
val localVersion = ChannelUtils.getLocalVersion(context)
//获取服务器版本号
val serverVersion = try {
ChannelUtils.getServerVersion(context)
} catch (e: IOException) {
Log.e("TVList", "无法从服务器获取版本信息", e)
Integer.MIN_VALUE
}
//频道列表
val channelTVMap: MutableMap<String, MutableList<TV>> = mutableMapOf()
//是否从服务器更新
var updateFromServer = false
//获取频道列表
val tvList: List<TV> = if (localVersion < serverVersion) {
//获取服务器地址
val url = ChannelUtils.getServerUrl(context)
//是否从服务器更新
updateFromServer = true
Log.i("TVList", "从服务器获取频道信息")
try {
ChannelUtils.getServerChannel(url)
} catch (e: IOException) {
Log.e("TVList", "无法从服务器获取频道信息", e)
updateFromServer = false
ChannelUtils.getLocalChannel(context)
}
} else {
Log.i("TVList", "从本地获取频道信息")
//获取本地频道
ChannelUtils.getLocalChannel(context)
}
//按频道分类
for (tv in tvList) {
val key = tv.channel
if (channelTVMap.containsKey(key)) {
val list = channelTVMap[key]!!
list.add(tv)
channelTVMap[key] = list
} else {
channelTVMap[key] = mutableListOf(tv)
}
}
//保存频道列表
list = channelTVMap
//保存版本号
if (updateFromServer) {
ChannelUtils.updateLocalChannel(context, serverVersion, tvList)
}
}
}
}
@@ -1,5 +0,0 @@
package com.lizongying.mytv
class UpdateManager {
}
+49 -39
View File
@@ -1,18 +1,12 @@
package com.lizongying.mytv
import android.content.Context
import android.content.res.Resources
import android.os.SystemClock
import android.util.Log
import android.util.TypedValue
import com.google.gson.Gson
import com.lizongying.mytv.api.TimeResponse
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.*
import java.io.IOException
object Utils {
fun getDateFormat(format: String): String {
@@ -23,35 +17,6 @@ object Utils {
return Date().time / 1000
}
init {
CoroutineScope(Dispatchers.Default).launch {
updateTimestampFromServer()
}
}
/**
* 从服务器获取时间戳
* @return Long 时间戳
*/
private suspend fun updateTimestampFromServer() {
val currentTimeMillis = withContext(Dispatchers.IO) {
val client = okhttp3.OkHttpClient.Builder()
.connectTimeout(500, java.util.concurrent.TimeUnit.MILLISECONDS)
.readTimeout(1, java.util.concurrent.TimeUnit.SECONDS).build()
client.newCall(
okhttp3.Request.Builder()
.url("https://api.m.taobao.com/rest/api3.do?api=mtop.common.getTimestamp")
.build()
).execute().use { response ->
if (!response.isSuccessful) throw java.io.IOException("Unexpected code $response")
val string = response.body()?.string()
Gson().fromJson(string, TimeResponse::class.java).data.t.toLong()
}
}
SystemClock.setCurrentTimeMillis(currentTimeMillis)
}
fun dpToPx(dp: Float): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().displayMetrics
@@ -63,4 +28,49 @@ object Utils {
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), Resources.getSystem().displayMetrics
).toInt()
}
/**
* 获取可读写的目录
*
* @param context 应用环境信息
*
* @return 可读写的目录
*
*/
fun getAppDirectory(context: Context): File {
return context.filesDir
}
/**
* 更新channels.json
*
* @param context 应用环境信息
*
* @return 无
*
* @throws IOException 网络请求失败
*/
fun updateChannel(context: Context) {
val client = okhttp3.OkHttpClient()
val request = okhttp3.Request.Builder().url(getServerUrl(context)).build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val body = response.body()
//覆盖channels.json
val file = File(getAppDirectory(context), "channels.json")
if (!file.exists()) {
file.createNewFile()
}
file.writeText(body!!.string())
}
}
/**
* 从res/values/server.xml获取服务器地址
* @param context 应用环境信息
* @return 服务器地址
*/
private fun getServerUrl(context: Context): String {
return context.resources.getString(R.string.server_url)
}
}
@@ -9,11 +9,3 @@ data class Info(
data class InfoData(
val token: String,
)
data class TimeResponse(
val data: Time
) {
data class Time(
val t: String
)
}
+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>
+1 -3
View File
@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="local_channel_version">1</integer>
<string name="server_version_url">https://raw.githubusercontent.com/LeGend-wLw/my-tv-json-utils/main/version.txt</string>
<string name="server_url">https://github.com/LeGend-wLw/my-tv-json-utils/raw/main/channels.json</string>
<string name="server_url">http://localhost:8080</string>
</resources>
+16 -1
View File
@@ -1,10 +1,25 @@
<resources>
<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 name="CustomImageCardTheme" parent="Theme.Leanback">
<item name="imageCardViewStyle">@style/CustomImageCardViewStyle</item>
<item name="imageCardViewImageStyle">@style/CustomImageCardViewImageStyle</item>
</style>
</resources>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB