diff --git a/app/build.gradle b/app/build.gradle index 0154aac..91ce467 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/java/com/lizongying/mytv/CardAdapter.kt b/app/src/main/java/com/lizongying/mytv/CardAdapter.kt new file mode 100644 index 0000000..9dbafdb --- /dev/null +++ b/app/src/main/java/com/lizongying/mytv/CardAdapter.kt @@ -0,0 +1,111 @@ +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() { + + 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" + } +} + diff --git a/app/src/main/java/com/lizongying/mytv/MainActivity.kt b/app/src/main/java/com/lizongying/mytv/MainActivity.kt index 7bddf48..7f00c9b 100644 --- a/app/src/main/java/com/lizongying/mytv/MainActivity.kt +++ b/app/src/main/java/com/lizongying/mytv/MainActivity.kt @@ -21,7 +21,6 @@ import android.widget.LinearLayout import android.widget.TextView import android.widget.Toast import androidx.core.content.ContextCompat -import androidx.core.view.setPadding import androidx.fragment.app.FragmentActivity import com.lizongying.mytv.models.TVViewModel import java.security.MessageDigest @@ -30,7 +29,7 @@ import java.security.MessageDigest class MainActivity : FragmentActivity() { var playerFragment = PlayerFragment() - private val mainFragment = MainFragment() + private val mainFragment = MainFragment2() private val infoFragment = InfoFragment() private var doubleBackToExitPressedOnce = false @@ -38,7 +37,7 @@ class MainActivity : FragmentActivity() { private lateinit var gestureDetector: GestureDetector private val handler = Handler() - private val delay: Long = 3000 + private val delay: Long = 4000 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -79,17 +78,18 @@ class MainActivity : FragmentActivity() { } private fun prevSource() { - mainFragment.prevSource() +// mainFragment.prevSource() } private fun nextSource() { - mainFragment.nextSource() +// mainFragment.nextSource() } fun switchMainFragment() { val transaction = supportFragmentManager.beginTransaction() if (mainFragment.isHidden) { + mainFragment.setPosition() transaction.show(mainFragment) keepRunnable() } else { diff --git a/app/src/main/java/com/lizongying/mytv/MainFragment.kt b/app/src/main/java/com/lizongying/mytv/MainFragment.kt index 18a941b..566d70c 100644 --- a/app/src/main/java/com/lizongying/mytv/MainFragment.kt +++ b/app/src/main/java/com/lizongying/mytv/MainFragment.kt @@ -209,8 +209,8 @@ class MainFragment : BrowseSupportFragment() { itemPosition = sharedPref?.getInt("position", 0)!! if (itemPosition >= tvListViewModel.size()) { itemPosition = 0 - tvListViewModel.setItemPosition(itemPosition) } + tvListViewModel.setItemPosition(itemPosition) } fun fragmentReady() { diff --git a/app/src/main/java/com/lizongying/mytv/MainFragment2.kt b/app/src/main/java/com/lizongying/mytv/MainFragment2.kt new file mode 100644 index 0000000..fefa654 --- /dev/null +++ b/app/src/main/java/com/lizongying/mytv/MainFragment2.kt @@ -0,0 +1,284 @@ +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 = 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 (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() + (activity as? MainActivity)?.switchMainFragment() + } + } + + 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" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lizongying/mytv/Utils.kt b/app/src/main/java/com/lizongying/mytv/Utils.kt index 241e536..dddda49 100644 --- a/app/src/main/java/com/lizongying/mytv/Utils.kt +++ b/app/src/main/java/com/lizongying/mytv/Utils.kt @@ -1,5 +1,7 @@ package com.lizongying.mytv +import android.content.res.Resources +import android.util.TypedValue import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -12,4 +14,20 @@ object Utils { fun getDateTimestamp(): Long { 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() + } } \ No newline at end of file diff --git a/app/src/main/res/layout/row.xml b/app/src/main/res/layout/row.xml new file mode 100644 index 0000000..6294a3e --- /dev/null +++ b/app/src/main/res/layout/row.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/show.xml b/app/src/main/res/layout/show.xml new file mode 100644 index 0000000..d635cec --- /dev/null +++ b/app/src/main/res/layout/show.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9b249ed..5a06b81 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,11 +1,25 @@ - + + \ No newline at end of file