Compare commits

..
80 Commits
Author SHA1 Message Date
Li ZongYing acff1d41fc x86 2024-06-20 14:20:22 +08:00
Li ZongYing ab71075c85 x86 2024-06-20 13:57:40 +08:00
Li ZongYing 5d4353249f fix ysp 2024-06-20 13:43:14 +08:00
Li ZongYing a8944e75b1 ysp 2024-06-08 11:23:31 +08:00
Li ZongYing d1eae8947e ysp 2024-06-08 10:50:22 +08:00
Li ZongYing fccd64c07b ysp 2024-06-08 10:49:10 +08:00
Li ZongYing eac355a1c1 fix black screen 2024-06-07 17:49:49 +08:00
Li ZongYing 6c6736fd0a gradle lib version 2024-06-07 17:22:42 +08:00
Li ZongYing f4c1db33b3 ysp 2024-06-07 13:13:05 +08:00
Li ZongYing e55584c910 Balanced sounds 2024-06-03 18:17:43 +08:00
Li ZongYing cf8d7f00a8 fix ysp 2024-05-31 11:10:18 +08:00
Li ZongYing bcfca9e28c fix gson crash 2024-05-27 14:13:34 +08:00
Li ZongYing 31ec13eed7 fix gson crash 2024-05-26 20:50:08 +08:00
Li ZongYing 5a78913407 fix gson crash 2024-05-26 20:49:01 +08:00
Li ZongYing bb38cf5033 fix gson crash 2024-05-26 20:20:42 +08:00
Li ZongYing 51d9c4c2a7 fix gson crash 2024-05-26 20:16:10 +08:00
Li ZongYing 70da1d8241 fix the settings options are not displayed 2024-05-26 09:36:24 +08:00
Li ZongYing ef17427f64 add reset 2024-05-21 09:55:15 +08:00
Li ZongYing c1143afb4e fix ysp 2024-05-20 22:23:16 +08:00
Li ZongYing 68f158e556 fix ysp 2024-05-20 22:08:37 +08:00
Li ZongYing 4ef95d60c4 fix ysp 2024-05-20 22:02:51 +08:00
Li ZongYing b782d1f841 fix some bug 2024-05-16 15:35:48 +08:00
Li ZongYing 7e57aa8c03 fix error in playing 2024-05-16 14:37:16 +08:00
Li ZongYing c16a4bcb9c optimize time 2024-05-09 22:26:37 +08:00
Li ZongYing 0286d118ed fix update 2024-05-07 17:55:08 +08:00
Li ZongYing fc83defe31 optimize version check 2024-05-07 12:56:34 +08:00
Li ZongYing 6fc73efd47 optimize version check 2024-05-07 12:55:11 +08:00
Li ZongYing b157f1eb28 fix time 2024-05-07 11:38:10 +08:00
Li ZongYing 2b172ae615 fix retry bug 2024-05-01 17:00:33 +08:00
Li ZongYing 39e5cc7c1f opitimize request 2024-05-01 16:52:33 +08:00
Li ZongYing df5b006024 optimize style 2024-04-29 12:12:59 +08:00
Li ZongYing fe888c36ac left->start 2024-04-27 19:11:08 +08:00
Li ZongYing 5aa08f51a1 error page 2024-04-26 19:57:12 +08:00
Li ZongYing fe92fa5853 optimize style 2024-04-25 22:36:37 +08:00
Li ZongYing 3d8ab65955 fix ysp 2024-04-23 19:41:30 +08:00
Li ZongYing 5485a686bc fix crash on 4.2 2024-04-19 20:13:43 +08:00
Li ZongYing 716efb5a83 change style 2024-04-19 19:26:06 +08:00
Li ZongYing ca82d5409f add time 2024-04-18 10:21:54 +08:00
Li ZongYing 3936629af8 add epg 2024-04-02 22:57:39 +08:00
Li ZongYing bb88e754a5 fix 368 2024-03-29 17:04:11 +08:00
Li ZongYing 49e96d18f4 fix version code 2024-03-17 09:38:46 +08:00
Li ZongYing 43343f8faa fix version code 2024-03-17 09:35:34 +08:00
Li ZongYing 0335f58a12 fix version name 2024-03-16 16:17:10 +08:00
Li ZongYing 9450de2c91 menu style 2024-03-16 13:14:14 +08:00
Li ZongYing d03de91c1e request optimize 2024-03-16 10:44:55 +08:00
Li ZongYing 7057e4a8b4 request optimize 2024-03-15 22:32:43 +08:00
Li ZongYing 188431a29f fix 4.0 crash 2024-03-14 22:08:28 +08:00
Li ZongYing 5c0152e36c optimize fetch token 2024-03-13 12:11:51 +08:00
Li ZongYing a63463559f phoenix retry 2024-03-13 11:31:37 +08:00
Li ZongYing d37ada1268 phoenix epg 2024-03-12 16:16:07 +08:00
Li ZongYing 00e6c8d66e cetv1 2024-03-07 19:37:03 +08:00
Li ZongYing 0ebc166acc bootStartup->false 2024-03-06 01:20:44 +08:00
Li ZongYing 2975a61e14 test RecyclerView alpha 2024-02-22 21:26:55 +08:00
Li ZongYing 86b9f5159a readme 2024-02-22 11:16:12 +08:00
Li ZongYing 10f261be8b fix save state 2024-02-21 21:57:05 +08:00
Li ZongYing 52d356817a fix some keys invalid 2024-02-21 19:26:11 +08:00
Li ZongYing 6ede7f76ef auto auth 2024-02-21 16:36:37 +08:00
Li ZongYing a4da01adf4 new memu style 2024-02-21 15:14:06 +08:00
Li ZongYing 8b33c7c94c optimize memu 2024-02-20 20:55:35 +08:00
Li ZongYing 84a6c0f78e menu delay disappears 2024-02-20 11:33:30 +08:00
Li ZongYing 1417bbf57b auto-startup 2024-02-19 15:57:39 +08:00
Li ZongYing ce678b8741 test release api 2024-02-11 10:14:23 +08:00
Li ZongYing 353dc0570f fix ysp 2024-02-09 22:36:48 +08:00
Li ZongYing d7a60531ab fix cctv6 2024-02-08 00:22:24 +08:00
Li ZongYing 764f2a1549 fix time error 2024-02-07 19:21:17 +08:00
Li ZongYing aa4d52681f fix ysp 2024-02-07 03:05:11 +08:00
Li ZongYing 8137515605 fix ysp err 2024-02-05 06:25:31 +08:00
Li ZongYing 6c16408b90 upgrade player 2024-02-04 22:23:28 +08:00
Li ZongYing 44ab3a6669 fix channel number did not disappear 2024-02-03 23:30:37 +08:00
Li ZongYing 917b245bbe readme 2024-02-02 23:34:39 +08:00
Li ZongYing dd4550cf4f replay on resume 2024-02-01 18:10:58 +08:00
Li ZongYing 4bdc625718 add channel 2024-01-26 18:30:31 +08:00
Li ZongYing cda19995b2 optimize channel switch 2024-01-25 15:12:47 +08:00
Li ZongYing 1eb2f257b5 change channel order 2024-01-25 11:04:06 +08:00
Li ZongYing 9feb0dc4bc channel num setting 2024-01-19 12:18:17 +08:00
Li ZongYing 46311036d6 number selection 2024-01-17 16:27:25 +08:00
Li ZongYing 1aade3a48d 17ok 2024-01-14 18:58:37 +08:00
Li ZongYing 2528cf33db test 17 2024-01-14 10:09:59 +08:00
Li ZongYing 4364f66ec5 17 2024-01-14 02:53:09 +08:00
Li ZongYing 49cd7b3af2 help page 2024-01-11 16:32:08 +08:00
86 changed files with 2868 additions and 1116 deletions
+16
View File
@@ -1,5 +1,21 @@
## 更新日誌
### v2.1.8
* 支持x86設備
### v2.1.7
*
### v2.1.4
* 解決切換頻道時黑屏問題
### v2.1.1
* 平衡聲音
### v2.1.0
* 恢復播放
+1 -10
View File
@@ -26,7 +26,7 @@
## TODO
* 音量不同
* 大湾区卫视、广东4k超高清、广东珠江、三沙卫视
* 大湾区卫视、广东4k超高清、广东珠江、三沙卫视、翡翠
* CHC高清三个电影频道
* 地方频道
* 收藏夹
@@ -35,18 +35,9 @@
* 亮度调节
* 音量调节
* 軟解
* 自動更新
無法自啟的設備:
斐讯N1盒子,[Phicomm] Phicomm p230 (Android 7.1.2)
閃退:
中国移动盒子(新魔百和M302A) 4.4.2
## 版权说明
[LICENSE](./LICENSE)
本项目仅供学习研究,禁止用于商业用途,请于下载二十四小时内删除。
本项目可能随时终止,请大家谨慎使用,建议使用官方渠道进行观看。
+1
View File
@@ -4,3 +4,4 @@
/CMakeFiles/
/Makefile
/src/main/cpp/native.c
/src/main/cpp/aes.c
+3 -1
View File
@@ -21,7 +21,9 @@ if (IS_SO_BUILD)
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native.c)
src/main/cpp/native.c
src/main/cpp/aes.c
)
# 设置编译输出路径
set_target_properties(
+29 -37
View File
@@ -13,7 +13,7 @@ android {
defaultConfig {
applicationId "com.lizongying.mytv"
minSdk 21
minSdk 17
targetSdk 33
versionCode VersionCode()
versionName VersionName()
@@ -26,7 +26,8 @@ android {
cmake {
arguments "-DIS_SO_BUILD=${project.hasProperty('IS_SO_BUILD') ? project.IS_SO_BUILD : true}"
abiFilters "armeabi-v7a", "arm64-v8a"
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
// abiFilters "armeabi-v7a", "arm64-v8a"
}
}
@@ -36,7 +37,8 @@ android {
ndk {
// Specifies the ABI configurations of your native
// libraries Gradle should build and package with your app.
abiFilters "armeabi-v7a", "arm64-v8a"
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
// abiFilters "armeabi-v7a", "arm64-v8a"
}
multiDexEnabled true
}
@@ -77,10 +79,9 @@ static def VersionCode() {
def replace = [v: "", ".": " ", "-": " "]
def arr = (process.text.trim().replace(replace) + " 0").split(" ")
def versionCode = arr[0].toInteger() * 16777216 + arr[1].toInteger() * 65536 + arr[2].toInteger() * 256 + arr[3].toInteger()
println("VersionCode $versionCode")
return versionCode
} catch (ignored) {
return 0
return 1
}
}
@@ -88,42 +89,33 @@ static def VersionName() {
try {
def process = "git describe --tags --always".execute()
process.waitFor()
return process.text.trim() - "v"
def versionName = process.text.trim() - "v"
if (versionName.isEmpty()) {
versionName = "1.0.0"
}
return versionName
} catch (ignored) {
return "1.0.0"
}
}
dependencies {
def media3_version = "1.3.1"
implementation "androidx.media3:media3-ui:$media3_version"
// For media playback using ExoPlayer
implementation "androidx.media3:media3-exoplayer:$media3_version"
// For HLS playback support with ExoPlayer
implementation "androidx.media3:media3-exoplayer-hls:$media3_version"
// 21:2.11.0 17:2.6.4
def retrofit2_version = "2.11.0"
implementation 'com.google.protobuf:protobuf-kotlin:3.25.1'
implementation "com.squareup.retrofit2:converter-gson:$retrofit2_version"
implementation "com.squareup.retrofit2:converter-protobuf:$retrofit2_version"
implementation "com.squareup.retrofit2:retrofit:$retrofit2_version"
implementation 'androidx.leanback:leanback:1.2.0-alpha04'
implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation 'androidx.core:core-ktx:1.13.0'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.7.0"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0-RC")
implementation 'com.google.android.exoplayer:exoplayer-ui:2.13.3'
implementation 'com.google.android.exoplayer:exoplayer-core:2.13.3'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.13.3'
implementation(libs.media3.ui)
implementation(libs.media3.exoplayer)
implementation(libs.media3.exoplayer.hls)
implementation(libs.protobuf.kotlin)
implementation(libs.gson)
implementation(libs.converter.gson)
implementation(libs.converter.protobuf)
implementation(libs.retrofit)
implementation(libs.glide)
implementation(libs.work.runtime.ktx)
implementation(libs.core.ktx)
implementation(libs.multidex)
implementation(libs.leanback)
implementation(libs.lifecycle.runtime.ktx)
implementation(libs.constraintlayout)
implementation(libs.serialization.json)
implementation(libs.coroutines.android)
implementation(libs.javax.annotation.api)
}
+18 -1
View File
@@ -10,11 +10,14 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:name=".MyApplication"
android:name=".MyTVApplication"
android:allowBackup="true"
android:banner="@drawable/banner"
android:icon="@drawable/logo"
@@ -36,6 +39,9 @@
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.windowLayoutInDisplayCutoutMode"
android:value="shortEdges" />
</activity>
<receiver
@@ -58,5 +64,16 @@
android:name=".InitializerProvider"
android:authorities="${applicationId}.InitializerProvider"
android:exported="false" />
<meta-data android:name="android.max_aspect" android:value="2.5" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
Binary file not shown.
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
#ifndef AES_H
#define AES_H
#include <stddef.h>
void init();
void encode(const char *in, size_t size, char *out);
#endif
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
@@ -0,0 +1,217 @@
package com.lizongying.mytv
import android.util.Log
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewGroup
import android.view.animation.ScaleAnimation
import androidx.core.view.marginTop
import androidx.core.view.setPadding
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.lizongying.mytv.databinding.CardBinding
import com.lizongying.mytv.models.TVListViewModel
import com.lizongying.mytv.models.TVViewModel
import kotlin.math.abs
class CardAdapter(
private val recyclerView: RecyclerView,
private val mainFragment: MainFragment,
private var tvListViewModel: TVListViewModel,
) :
RecyclerView.Adapter<CardAdapter.ViewHolder>() {
private var listener: ItemListener? = null
private var focused: View? = null
var focusable = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = CardBinding.inflate(inflater, parent, false)
val application = mainFragment.requireActivity().applicationContext as MyTVApplication
binding.card.layoutParams.width = application.px2Px(binding.card.layoutParams.width)
binding.card.layoutParams.height = application.px2Px(binding.card.layoutParams.height)
binding.icon.layoutParams.height = application.px2Px(binding.icon.layoutParams.height)
binding.icon.setPadding(application.px2Px(binding.icon.paddingTop))
binding.main.layoutParams.height = application.px2Px(binding.main.layoutParams.height)
binding.main.setPadding(application.px2Px(binding.main.paddingTop))
binding.title.textSize = application.px2PxFont(binding.title.textSize)
binding.desc.textSize = application.px2PxFont(binding.desc.textSize)
val layoutParams = binding.desc.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.topMargin = application.px2Px(binding.desc.marginTop)
binding.desc.layoutParams = layoutParams
val cardView = binding.root
cardView.isClickable = true
cardView.isFocusable = true
cardView.isFocusableInTouchMode = true
return ViewHolder(cardView, binding)
}
fun clear() {
focused?.clearFocus()
recyclerView.invalidate()
}
private fun startScaleAnimation(view: View) {
val scaleAnimation = ScaleAnimation(
0.9f, 1.0f,
0.9f, 1.0f,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f
)
scaleAnimation.duration = 200
scaleAnimation.fillAfter = false
view.startAnimation(scaleAnimation)
}
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
val item = tvListViewModel.getTVViewModel(position)
val tvViewModel = item as TVViewModel
val cardView = viewHolder.view
cardView.tag = tvViewModel
val onFocusChangeListener = View.OnFocusChangeListener { view, hasFocus ->
if (hasFocus) {
listener?.onItemHasFocus(item)
focused = cardView
if (focusable) {
startScaleAnimation(cardView)
viewHolder.focus(true)
}
} else {
viewHolder.focus(false)
}
}
cardView.onFocusChangeListener = onFocusChangeListener
cardView.setOnClickListener { _ ->
listener?.onItemClicked(item)
}
var downX = 0f
var downY = 0f
val touchSlop = ViewConfiguration.get(cardView.context).scaledTouchSlop
cardView.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
downX = event.x
downY = event.y
true
}
MotionEvent.ACTION_UP -> {
val upX = event.x
val upY = event.y
val deltaX = abs(upX - downX)
val deltaY = abs(upY - downY)
if (deltaX < touchSlop && deltaY < touchSlop) {
// 如果位移量小于阈值,则执行点击操作
cardView.performClick()
}
true
}
else -> false
}
}
cardView.setOnKeyListener { _, keyCode, event: KeyEvent? ->
if (event?.action == KeyEvent.ACTION_DOWN) {
return@setOnKeyListener listener?.onKey(keyCode) ?: false
}
false
}
viewHolder.binding.title.text = tvViewModel.getTV().title
viewHolder.binding.icon.let {
Glide.with(viewHolder.view.context)
.load(tvViewModel.getTV().logo)
.centerInside()
.into(it)
}
val epg = tvViewModel.epg.value?.filter { it.beginTime < Utils.getDateTimestamp() }
if (!epg.isNullOrEmpty()) {
viewHolder.binding.desc.text = epg.last().title
} else {
viewHolder.binding.desc.text = ""
}
}
override fun getItemCount() = tvListViewModel.size()
class ViewHolder(itemView: View, var binding: CardBinding) : RecyclerView.ViewHolder(itemView) {
val view = itemView
fun focus(hasFocus: Boolean) {
if (hasFocus) {
binding.main.setBackgroundResource(R.drawable.rounded_light_bottom)
} else {
binding.main.setBackgroundResource(R.drawable.rounded_dark_bottom)
}
}
}
fun updateEPG() {
for (i in 0 until recyclerView.childCount) {
val childView = recyclerView.getChildAt(i)
val viewHolder = recyclerView.getChildViewHolder(childView) as ViewHolder
if (viewHolder.view.tag != null && viewHolder.view.tag is TVViewModel) {
val tvViewModel = viewHolder.view.tag as TVViewModel
val epg = tvViewModel.epg.value?.filter { it.beginTime < Utils.getDateTimestamp() }
if (!epg.isNullOrEmpty()) {
val title = epg.last().title
if (viewHolder.binding.desc.text != title) {
viewHolder.binding.desc.text = title
Log.i(TAG, "updateEPG $title")
}
} else {
if (viewHolder.binding.desc.text != "") {
viewHolder.binding.desc.text = ""
Log.i(TAG, "updateEPG")
}
}
}
}
}
fun toPosition(position: Int) {
recyclerView.post {
recyclerView.scrollToPosition(position)
recyclerView.getChildAt(position)?.isSelected
recyclerView.getChildAt(position)?.requestFocus()
}
}
interface ItemListener {
fun onItemHasFocus(tvViewModel: TVViewModel)
fun onItemClicked(tvViewModel: TVViewModel)
fun onKey(keyCode: Int): Boolean
}
fun setItemListener(listener: ItemListener) {
this.listener = listener
}
companion object {
private const val TAG = "CardAdapter"
}
}
@@ -1,76 +0,0 @@
package com.lizongying.mytv
import android.content.Context
import android.graphics.Color
import android.view.ContextThemeWrapper
import android.view.ViewGroup
import android.widget.ImageView
import androidx.leanback.widget.ImageCardView
import androidx.leanback.widget.Presenter
import com.bumptech.glide.Glide
import com.lizongying.mytv.models.TVViewModel
class CardPresenter(
private val context: Context,
) : Presenter() {
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
val cardView = object :
ImageCardView(ContextThemeWrapper(parent.context, R.style.CustomImageCardTheme)) {}
cardView.isFocusable = true
cardView.isFocusableInTouchMode = true
return ViewHolder(cardView)
}
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) {
val tvViewModel = item as TVViewModel
val cardView = viewHolder.view as ImageCardView
cardView.titleText = tvViewModel.getTV().title
cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT)
cardView.tag = tvViewModel.videoUrl.value
cardView.mainImageView?.let {
Glide.with(viewHolder.view.context)
.load(tvViewModel.getTV().logo)
.centerInside()
.into(it)
}
cardView.setBackgroundColor(Color.WHITE)
cardView.setMainImageScaleType(ImageView.ScaleType.CENTER_INSIDE)
// cardView.setOnFocusChangeListener { v, hasFocus ->
// run {
// if (hasFocus) {
// if (v != null) {
// (v as ImageCardView).setInfoAreaBackgroundColor(context.resources.getColor(R.color.focus))
// }
// } else {
// if (v != null) {
// (v as ImageCardView).setInfoAreaBackgroundColor(context.resources.getColor(R.color.ic_launcher_background))
// }
// }
// }
// }
val epg = tvViewModel.epg.value?.filter { it.beginTime < Utils.getDateTimestamp() }
if (!epg.isNullOrEmpty()) {
cardView.contentText = epg.last().title
} else {
cardView.contentText = ""
}
}
override fun onUnbindViewHolder(viewHolder: ViewHolder) {
val cardView = viewHolder.view as ImageCardView
cardView.mainImage = null
}
companion object {
private const val TAG = "CardPresenter"
private const val CARD_WIDTH = 300
private const val CARD_HEIGHT = 101
}
}
@@ -1,11 +1,13 @@
package com.lizongying.mytv
import android.content.res.Resources
import android.os.Bundle
import android.os.Handler
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.marginEnd
import androidx.core.view.marginTop
import androidx.fragment.app.Fragment
import com.lizongying.mytv.databinding.ChannelBinding
import com.lizongying.mytv.models.TVViewModel
@@ -15,7 +17,7 @@ class ChannelFragment : Fragment() {
private val binding get() = _binding!!
private val handler = Handler()
private val delay: Long = 3000
private val delay: Long = 5000
private var channel = 0
private var channelCount = 0
@@ -26,45 +28,30 @@ class ChannelFragment : Fragment() {
_binding = ChannelBinding.inflate(inflater, container, false)
_binding!!.root.visibility = View.GONE
val activity = requireActivity()
val application = activity.applicationContext as MyApplication
val displayMetrics = application.getDisplayMetrics()
val application = requireActivity().applicationContext as MyTVApplication
displayMetrics.density
binding.channel.layoutParams.width = application.px2Px(binding.channel.layoutParams.width)
binding.channel.layoutParams.height = application.px2Px(binding.channel.layoutParams.height)
var screenWidth = displayMetrics.widthPixels
var screenHeight = displayMetrics.heightPixels
if (screenHeight > screenWidth) {
screenWidth = displayMetrics.heightPixels
screenHeight = displayMetrics.widthPixels
}
val layoutParams = binding.channel.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.topMargin = application.px2Px(binding.channel.marginTop)
layoutParams.marginEnd = application.px2Px(binding.channel.marginEnd)
binding.channel.layoutParams = layoutParams
val ratio = 16f / 9f
binding.content.textSize = application.px2PxFont(binding.content.textSize)
binding.time.textSize = application.px2PxFont(binding.time.textSize)
if (screenWidth / screenHeight > ratio) {
val x = ((screenWidth - screenHeight * ratio) / 2).toInt()
val originalLayoutParams =
binding.channelFragment.layoutParams as ViewGroup.MarginLayoutParams
originalLayoutParams.rightMargin += x
binding.channelFragment.layoutParams = originalLayoutParams
}
binding.main.layoutParams.width = application.shouldWidthPx()
binding.main.layoutParams.height = application.shouldHeightPx()
if (screenWidth / screenHeight < ratio) {
val y = ((screenHeight - screenWidth / ratio) / 2).toInt()
val originalLayoutParams =
binding.channelFragment.layoutParams as ViewGroup.MarginLayoutParams
originalLayoutParams.topMargin += y
binding.channelFragment.layoutParams = originalLayoutParams
}
(activity as MainActivity).fragmentReady("ChannelFragment")
(activity as MainActivity).fragmentReady(TAG)
return binding.root
}
fun show(tvViewModel: TVViewModel) {
handler.removeCallbacks(hideRunnable)
handler.removeCallbacks(playRunnable)
binding.channelContent.text = (tvViewModel.getTV().id.plus(1)).toString()
binding.content.text = (tvViewModel.getTV().id.plus(1)).toString()
view?.visibility = View.VISIBLE
handler.postDelayed(hideRunnable, delay)
}
@@ -74,13 +61,11 @@ class ChannelFragment : Fragment() {
return
}
channelCount++
Log.i(TAG, "channelCount ${channelCount}")
this.channel = "${this.channel}$channel".toInt()
Log.i(TAG, "this.channel ${this.channel}")
handler.removeCallbacks(hideRunnable)
handler.removeCallbacks(playRunnable)
if (channelCount < 2) {
binding.channelContent.text = "${this.channel}"
binding.content.text = "${this.channel}"
view?.visibility = View.VISIBLE
handler.postDelayed(playRunnable, delay)
} else {
@@ -102,20 +87,18 @@ class ChannelFragment : Fragment() {
}
private val hideRunnable = Runnable {
binding.channelContent.text = ""
binding.content.text = ""
view?.visibility = View.GONE
channel = 0
channelCount = 0
Log.i(TAG, "hideRunnable")
}
private val playRunnable = Runnable {
(activity as MainActivity).play(channel - 1)
binding.channelContent.text = ""
binding.content.text = ""
view?.visibility = View.GONE
channel = 0
channelCount = 0
Log.i(TAG, "playRunnable")
}
override fun onDestroyView() {
@@ -3,28 +3,61 @@ package com.lizongying.mytv
import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import com.lizongying.mytv.databinding.ConfirmationBinding
class ConfirmationFragment(
private val listener: ConfirmationListener,
private val message: String
private val message: String,
private val update: Boolean
) : DialogFragment() {
//
// private var _binding: ConfirmationBinding? = null
// private val binding get() = _binding!!
//
// override fun onCreateView(
// inflater: LayoutInflater, container: ViewGroup?,
// savedInstanceState: Bundle?
// ): View {
// _binding = ConfirmationBinding.inflate(inflater, container, false)
// return binding.root
// }
//
// fun updateProgress(progress: Int) {
// binding.progressBar.progress = progress
// }
//
// override fun onDestroyView() {
// super.onDestroyView()
// _binding = null
// }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
builder.setTitle("确定更新吗?")
.setMessage(message)
.setPositiveButton(
"确定"
) { _, _ ->
listener.onConfirm()
}
.setNegativeButton(
"取消"
) { _, _ ->
listener.onCancel()
}
builder.setTitle(message)
if (update) {
builder.setMessage("确定更新吗?")
.setPositiveButton(
"确定"
) { _, _ ->
listener.onConfirm()
}
.setNegativeButton(
"取消"
) { _, _ ->
listener.onCancel()
}
} else {
builder.setMessage("")
.setNegativeButton(
"好的"
) { _, _ ->
}
}
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
@@ -1,28 +1,49 @@
package com.lizongying.mytv
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.core.content.ContextCompat
import androidx.leanback.app.ErrorSupportFragment
import android.view.ViewGroup
import androidx.core.view.marginStart
import androidx.core.view.marginTop
import androidx.fragment.app.Fragment
import com.lizongying.mytv.databinding.ErrorBinding
class ErrorFragment : ErrorSupportFragment() {
class ErrorFragment : Fragment() {
private var _binding: ErrorBinding? = null
private val binding get() = _binding!!
internal fun setErrorContent(message: String) {
imageDrawable =
ContextCompat.getDrawable(context!!, androidx.leanback.R.drawable.lb_ic_sad_cloud)
this.message = message
setDefaultBackground(TRANSLUCENT)
backgroundDrawable = ContextCompat.getDrawable(
context!!,
R.color.black
)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = ErrorBinding.inflate(inflater, container, false)
buttonText = resources.getString(R.string.dismiss_error)
buttonClickListener = View.OnClickListener {
fragmentManager!!.beginTransaction().remove(this@ErrorFragment).commit()
}
val application = requireActivity().applicationContext as MyTVApplication
binding.logo.layoutParams.width = application.px2Px(binding.logo.layoutParams.width)
binding.logo.layoutParams.height = application.px2Px(binding.logo.layoutParams.height)
val layoutParams = binding.msg.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.topMargin = application.px2Px(binding.msg.marginTop)
binding.msg.layoutParams = layoutParams
binding.msg.textSize = application.px2PxFont(binding.msg.textSize)
(activity as MainActivity).fragmentReady(TAG)
return binding.root
}
fun show(msg: String) {
binding.msg.text = msg
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val TRANSLUCENT = false
private const val TAG = "ErrorFragment"
}
}
@@ -1,11 +1,16 @@
package com.lizongying.mytv
import android.content.res.Resources
import android.os.Bundle
import android.os.Handler
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.marginBottom
import androidx.core.view.marginStart
import androidx.core.view.marginTop
import androidx.core.view.setPadding
import androidx.fragment.app.Fragment
import com.bumptech.glide.Glide
import com.lizongying.mytv.databinding.InfoBinding
@@ -16,31 +21,64 @@ class InfoFragment : Fragment() {
private val binding get() = _binding!!
private val handler = Handler()
private val delay: Long = 3000
private val delay: Long = 5000
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = InfoBinding.inflate(inflater, container, false)
val application = requireActivity().applicationContext as MyTVApplication
binding.info.layoutParams.width = application.px2Px(binding.info.layoutParams.width)
binding.info.layoutParams.height = application.px2Px(binding.info.layoutParams.height)
val layoutParams = binding.info.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.bottomMargin = application.px2Px(binding.info.marginBottom)
binding.info.layoutParams = layoutParams
binding.logo.layoutParams.width = application.px2Px(binding.logo.layoutParams.width)
binding.logo.setPadding(application.px2Px(binding.logo.paddingTop))
binding.main.layoutParams.width = application.px2Px(binding.main.layoutParams.width)
binding.main.setPadding(application.px2Px(binding.main.paddingTop))
val layoutParamsMain = binding.main.layoutParams as ViewGroup.MarginLayoutParams
layoutParamsMain.marginStart = application.px2Px(binding.main.marginStart)
binding.main.layoutParams = layoutParamsMain
val layoutParamsDesc = binding.desc.layoutParams as ViewGroup.MarginLayoutParams
layoutParamsDesc.topMargin = application.px2Px(binding.desc.marginTop)
binding.desc.layoutParams = layoutParamsDesc
binding.title.textSize = application.px2PxFont(binding.title.textSize)
binding.desc.textSize = application.px2PxFont(binding.desc.textSize)
binding.container.layoutParams.width = application.shouldWidthPx()
binding.container.layoutParams.height = application.shouldHeightPx()
_binding!!.root.visibility = View.GONE
(activity as MainActivity).fragmentReady("InfoFragment")
(activity as MainActivity).fragmentReady(TAG)
return binding.root
}
fun show(tvViewModel: TVViewModel) {
binding.textView.text = tvViewModel.getTV().title
if (_binding == null) {
return
}
binding.title.text = tvViewModel.getTV().title
Glide.with(this)
.load(tvViewModel.getTV().logo)
.into(binding.infoLogo)
.into(binding.logo)
Log.i(TAG, "${tvViewModel.getTV().title} ${tvViewModel.epg.value}")
val epg = tvViewModel.epg.value?.filter { it.beginTime < Utils.getDateTimestamp() }
if (!epg.isNullOrEmpty()) {
binding.infoDesc.text = epg.last().title
binding.desc.text = epg.last().title
} else {
binding.infoDesc.text = ""
binding.desc.text = ""
}
handler.removeCallbacks(removeRunnable)
@@ -0,0 +1,29 @@
package com.lizongying.mytv
import android.content.Context
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class ItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
private var application: MyTVApplication
init {
application = context.applicationContext as MyTVApplication
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
val space = application.dp2Px(5)
outRect.top = space
outRect.bottom = space
outRect.left = space
outRect.right = space
}
}
@@ -0,0 +1,37 @@
package com.lizongying.mytv
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.lizongying.mytv.databinding.LoadingBinding
class LoadingFragment : Fragment() {
private var _binding: LoadingBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = LoadingBinding.inflate(inflater, container, false)
val application = requireActivity().applicationContext as MyTVApplication
binding.bar.layoutParams.width = application.px2Px(binding.bar.layoutParams.width)
binding.bar.layoutParams.height = application.px2Px(binding.bar.layoutParams.height)
(activity as MainActivity).fragmentReady(TAG)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val TAG = "LoadingFragment"
}
}
@@ -1,8 +1,5 @@
package com.lizongying.mytv
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.os.Build
import android.os.Bundle
import android.os.Handler
@@ -12,28 +9,35 @@ import android.view.GestureDetector
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.View.SYSTEM_UI_FLAG_FULLSCREEN
import android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
import android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
import android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
import android.view.WindowManager
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.lizongying.mytv.models.TVViewModel
import com.lizongying.mytv.requests.Request
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
class MainActivity : FragmentActivity(), Request.RequestListener {
class MainActivity : FragmentActivity(), Request.RequestListener, OnSharedPreferenceChangeListener {
private var ready = 0
private val playerFragment = PlayerFragment()
private val errorFragment = ErrorFragment()
private val loadingFragment = LoadingFragment()
private val mainFragment = MainFragment()
private val infoFragment = InfoFragment()
private val channelFragment = ChannelFragment()
private var timeFragment = TimeFragment()
private val settingFragment = SettingFragment()
private val errorFragment = ErrorFragment()
private var doubleBackToExitPressedOnce = false
@@ -41,67 +45,98 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
private val handler = Handler()
private val delayHideMain: Long = 10000
private val delayHideSetting: Long = 10000
private val delayHideSetting: Long = 15000
init {
Utils.setRequestListener(this)
}
fun syncTime() {
lifecycleScope.launch(Dispatchers.IO) {
val utilsJob = async(start = CoroutineStart.LAZY) { Utils.init() }
utilsJob.start()
// utilsJob.await()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
Log.i(TAG, "onCreate")
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val lp = window.attributes
lp.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
window.setAttributes(lp)
}
window.decorView.apply {
systemUiVisibility =
SYSTEM_UI_FLAG_FULLSCREEN or
SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
SYSTEM_UI_FLAG_HIDE_NAVIGATION or
SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_IMMERSIVE
}
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
window.decorView.systemUiVisibility =
SYSTEM_UI_FLAG_FULLSCREEN or
SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or SYSTEM_UI_FLAG_HIDE_NAVIGATION or
SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
// window.statusBarColor = Color.TRANSPARENT
// window.navigationBarColor = Color.TRANSPARENT
// }
setContentView(R.layout.activity_main)
Request.setRequestListener(this)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
window.decorView.systemUiVisibility = SYSTEM_UI_FLAG_HIDE_NAVIGATION
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
val transaction = supportFragmentManager.beginTransaction()
.add(R.id.main_browse_fragment, playerFragment)
.add(R.id.main_browse_fragment, errorFragment)
// .add(R.id.main_browse_fragment, loadingFragment)
.add(R.id.main_browse_fragment, timeFragment)
.add(R.id.main_browse_fragment, infoFragment)
.add(R.id.main_browse_fragment, channelFragment)
.add(R.id.main_browse_fragment, mainFragment)
.hide(mainFragment)
.commit()
.hide(errorFragment)
if (!SP.time) {
transaction.hide(timeFragment)
} else {
transaction.show(timeFragment)
}
transaction.commitNow()
}
gestureDetector = GestureDetector(this, GestureListener())
errorFragment.buttonClickListener = View.OnClickListener {
supportFragmentManager.beginTransaction()
.remove(errorFragment)
.commit()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val connectivityManager =
getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
connectivityManager.registerDefaultNetworkCallback(object :
ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
Log.i(TAG, "net ${Build.VERSION.SDK_INT}")
if (this@MainActivity.isNetworkConnected) {
Log.i(TAG, "net isNetworkConnected")
ready++
}
}
})
} else {
Log.i(TAG, "net ${Build.VERSION.SDK_INT}")
ready++
}
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// val connectivityManager =
// getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
// connectivityManager.registerDefaultNetworkCallback(object :
// ConnectivityManager.NetworkCallback() {
// override fun onAvailable(network: Network) {
// super.onAvailable(network)
// Log.i(TAG, "net ${Build.VERSION.SDK_INT}")
// if (this@MainActivity.isNetworkConnected) {
// Log.i(TAG, "net isNetworkConnected")
// ready++
// }
// }
// })
// } else {
// Log.i(TAG, "net ${Build.VERSION.SDK_INT}")
// ready++
// }
SP.setOnSharedPreferenceChangeListener(this)
}
fun showInfoFragment(tvViewModel: TVViewModel) {
@@ -111,6 +146,52 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
}
}
fun showErrorFragment(msg: String) {
errorFragment.show(msg)
if (errorFragment.isVisible) {
return
}
supportFragmentManager.beginTransaction()
.show(errorFragment)
.commit()
}
fun hideErrorFragment() {
errorFragment.show("")
if (!errorFragment.isVisible) {
return
}
supportFragmentManager.beginTransaction()
.hide(errorFragment)
.commit()
}
fun showLoadingFragment() {
showFragment(loadingFragment)
}
fun hideLoadingFragment() {
hideFragment(loadingFragment)
}
fun showPlayerFragment() {
if (playerFragment.isVisible) {
return
}
supportFragmentManager.beginTransaction()
.show(playerFragment)
.commit()
}
fun hidePlayerFragment() {
if (!playerFragment.isVisible) {
return
}
supportFragmentManager.beginTransaction()
.hide(playerFragment)
.commit()
}
private fun showChannel(channel: String) {
if (!mainFragment.isHidden) {
return
@@ -127,7 +208,6 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
fun play(tvViewModel: TVViewModel) {
playerFragment.play(tvViewModel)
mainFragment.view?.requestFocus()
}
fun play(itemPosition: Int) {
@@ -142,18 +222,11 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
mainFragment.next()
}
private fun prevSource() {
// mainFragment.prevSource()
}
private fun nextSource() {
// mainFragment.nextSource()
}
fun switchMainFragment() {
val transaction = supportFragmentManager.beginTransaction()
if (mainFragment.isHidden) {
mainFragment.setPosition()
transaction.show(mainFragment)
mainActive()
} else {
@@ -171,12 +244,6 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
fun settingDelayHide() {
handler.removeCallbacks(hideSetting)
handler.postDelayed(hideSetting, delayHideSetting)
showTime()
}
fun settingHideNow() {
handler.removeCallbacks(hideSetting)
handler.postDelayed(hideSetting, 0)
}
fun settingNeverHide() {
@@ -204,27 +271,37 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
fun fragmentReady(tag: String) {
ready++
Log.i(TAG, "ready $tag $ready ")
if (ready == 6) {
if (ready == 7) {
mainFragment.fragmentReady()
showTime()
}
}
private fun showTime() {
Log.i(TAG, "showTime ${SP.time}")
if (SP.time) {
timeFragment.show()
showFragment(timeFragment)
} else {
timeFragment.hide()
hideFragment(timeFragment)
}
}
fun isPlaying() {
if (errorFragment.isVisible) {
supportFragmentManager.beginTransaction()
.remove(errorFragment)
.commit()
private fun showFragment(fragment: Fragment) {
if (!fragment.isHidden) {
return
}
supportFragmentManager.beginTransaction()
.show(fragment)
.commitNow()
}
private fun hideFragment(fragment: Fragment) {
if (fragment.isHidden) {
return
}
supportFragmentManager.beginTransaction()
.hide(fragment)
.commitNow()
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
@@ -237,6 +314,7 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
Log.i(TAG, "onSingleTapConfirmed")
switchMainFragment()
return true
}
@@ -284,14 +362,12 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
return
}
Log.i(TAG, "settingFragment ${settingFragment.isVisible}")
if (!settingFragment.isVisible) {
settingFragment.show(supportFragmentManager, "setting")
settingDelayHide()
} else {
handler.removeCallbacks(hideSetting)
settingFragment.dismiss()
if (settingFragment.isVisible) {
return
}
settingFragment.show(supportFragmentManager, TAG)
settingDelayHide()
}
private val hideSetting = Runnable {
@@ -300,6 +376,10 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
}
}
private fun hideSettingFragment() {
handler.postDelayed(hideSetting, 0)
}
private fun channelUp() {
if (mainFragment.isHidden) {
if (SP.channelReversal) {
@@ -339,6 +419,11 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
return
}
if (settingFragment.isVisible) {
hideSettingFragment()
return
}
if (doubleBackToExitPressedOnce) {
super.onBackPressed()
return
@@ -352,8 +437,14 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
}, 2000)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
Log.i(TAG, "keyCode $keyCode, event $event")
private fun active() {
if (settingFragment.isVisible) {
settingDelayHide()
}
}
fun onKey(keyCode: Int): Boolean {
Log.i(TAG, "keyCode $keyCode")
when (keyCode) {
KeyEvent.KEYCODE_0 -> {
showChannel("0")
@@ -421,7 +512,7 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
}
KeyEvent.KEYCODE_UNKNOWN -> {
showSetting()
// showSetting()
return true
}
@@ -441,6 +532,7 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
}
KeyEvent.KEYCODE_ENTER -> {
active()
switchMainFragment()
}
@@ -449,6 +541,7 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
}
KeyEvent.KEYCODE_DPAD_UP -> {
active()
channelUp()
}
@@ -457,6 +550,7 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
active()
channelDown()
}
@@ -465,19 +559,28 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
}
KeyEvent.KEYCODE_DPAD_LEFT -> {
if (!mainFragment.isVisible && !settingFragment.isVisible) {
active()
if (mainFragment.isHidden && !settingFragment.isVisible) {
switchMainFragment()
return true
}
}
KeyEvent.KEYCODE_DPAD_RIGHT -> {
if (!mainFragment.isVisible && !settingFragment.isVisible) {
active()
if (mainFragment.isHidden && !settingFragment.isVisible) {
showSetting()
return true
}
}
}
return false
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (onKey(keyCode)) {
return true
}
return super.onKeyDown(keyCode, event)
}
@@ -485,13 +588,20 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
private fun getAppSignature() = this.appSignature
override fun onStart() {
Log.i(TAG, "onStart")
Log.i(TAG, "onStart MainActivity")
syncTime()
super.onStart()
}
override fun onResume() {
Log.i(TAG, "onResume")
super.onResume()
}
override fun onResumeFragments() {
super.onResumeFragments()
mainFragment.changeMenu()
if (!mainFragment.isHidden) {
handler.postDelayed(hideMain, delayHideMain)
}
@@ -509,16 +619,18 @@ class MainActivity : FragmentActivity(), Request.RequestListener {
}
override fun onRequestFinished(message: String?) {
if (message != null && !errorFragment.isVisible) {
supportFragmentManager.beginTransaction()
.add(R.id.main_browse_fragment, errorFragment)
.commitNow()
errorFragment.setErrorContent(message)
}
fragmentReady("Request")
fragmentReady("request")
}
private companion object {
const val TAG = "MainActivity"
}
override fun onSharedPreferenceChanged(key: String) {
Log.i(TAG, "$key changed")
when (key) {
SP.KEY_GRID -> mainFragment.changeMenu()
SP.KEY_TIME -> showTime()
}
}
}
@@ -2,71 +2,83 @@ package com.lizongying.mytv
import android.os.Bundle
import android.util.Log
import android.view.GestureDetector
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
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.core.view.marginBottom
import androidx.core.view.marginStart
import androidx.core.view.marginTop
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.lizongying.mytv.api.YSP
import com.lizongying.mytv.databinding.MenuBinding
import com.lizongying.mytv.databinding.RowBinding
import com.lizongying.mytv.models.ProgramType
import com.lizongying.mytv.models.TVList
import com.lizongying.mytv.models.TVListViewModel
import com.lizongying.mytv.models.TVViewModel
import com.lizongying.mytv.requests.Request
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: MenuBinding? = null
private val binding get() = _binding!!
var tvListViewModel = TVListViewModel()
private var lastVideoUrl = ""
override fun onCreate(savedInstanceState: Bundle?) {
Log.i(TAG, "onCreate")
super.onCreate(savedInstanceState)
headersState = HEADERS_DISABLED
private lateinit var application: MyTVApplication
private lateinit var gestureDetector: GestureDetector
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val context = requireContext()
_binding = MenuBinding.inflate(inflater, container, false)
application = requireActivity().applicationContext as MyTVApplication
binding.menu.layoutParams.width = application.shouldWidthPx()
binding.menu.layoutParams.height = application.shouldHeightPx()
binding.container.setOnClickListener {
hideSelf()
}
gestureDetector = GestureDetector(context, GestureListener())
return binding.root
}
// override fun onCreateView(
// inflater: LayoutInflater,
// container: ViewGroup?,
// savedInstanceState: Bundle?
// ): View? {
// val rootView = super.onCreateView(inflater, container, savedInstanceState)
// rootView?.setOnClickListener {
// Log.i(TAG, "main on click")
// fragmentManager!!.beginTransaction().hide(this).commit()
// }
// mainFragment.view?.setOnClickListener {
// Log.i(TAG, "mainFragment on click")
// fragmentManager!!.beginTransaction().hide(this).commit()
// }
// getRowsSupportFragment().view?.setOnClickListener {
// Log.i(TAG, "getRowsSupportFragment on click")
// fragmentManager!!.beginTransaction().hide(this).commit()
// }
//
//
// return rootView
// }
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
Log.i(TAG, "onSingleTapConfirmed")
hideSelf()
return true
}
}
override fun onStart() {
Log.i(TAG, "onStart")
super.onStart()
private fun hideSelf() {
requireActivity().supportFragmentManager.beginTransaction()
.hide(this)
.commit()
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
@@ -74,168 +86,247 @@ class MainFragment : BrowseSupportFragment() {
activity?.let { YSP.init(it) }
loadRows()
setupEventListeners()
tvListViewModel.tvListViewModel.value?.forEach { tvViewModel ->
tvViewModel.errInfo.observe(viewLifecycleOwner) { _ ->
if (tvViewModel.errInfo.value != null
&& tvViewModel.getTV().id == 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.getTV().id == itemPosition
&& check(tvViewModel)
) {
Log.i(TAG, "ready ${tvViewModel.getTV().title}")
(activity as? MainActivity)?.play(tvViewModel)
}
}
tvViewModel.change.observe(viewLifecycleOwner) { _ ->
if (tvViewModel.change.value != null) {
val title = tvViewModel.getTV().title
Log.i(TAG, "switch $title")
if (tvViewModel.getTV().pid != "") {
Log.i(TAG, "request $title")
lifecycleScope.launch(Dispatchers.IO) {
tvViewModel.let { Request.fetchData(it) }
}
(activity as? MainActivity)?.showInfoFragment(tvViewModel)
setSelectedPosition(
tvViewModel.getRowPosition(), true,
SelectItemViewHolderTask(tvViewModel.getItemPosition())
)
} else {
if (check(tvViewModel)) {
(activity as? MainActivity)?.play(tvViewModel)
(activity as? MainActivity)?.showInfoFragment(tvViewModel)
setSelectedPosition(
tvViewModel.getRowPosition(), true,
SelectItemViewHolderTask(tvViewModel.getItemPosition())
)
}
}
}
}
}
(activity as MainActivity).fragmentReady("MainFragment")
}
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(context!!)
var idx: Long = 0
for ((k, v) in TVList.list) {
val listRowAdapter = ArrayObjectAdapter(cardPresenter)
for ((idx2, v1) in v.withIndex()) {
val tvViewModel = TVViewModel(v1)
tvViewModel.setRowPosition(idx.toInt())
tvViewModel.setItemPosition(idx2)
tvListViewModel.addTVViewModel(tvViewModel)
listRowAdapter.add(tvViewModel)
}
tvListViewModel.maxNum.add(v.size)
val header = HeaderItem(idx, k)
rowsAdapter!!.add(ListRow(header, listRowAdapter))
idx++
}
adapter = rowsAdapter
itemPosition = SP.itemPosition
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)
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(
itemBinding.items,
this,
tvListViewModelCurrent,
)
rowList.add(itemBinding.items)
adapter.setItemListener(this)
itemBinding.header.text = k
itemBinding.items.tag = idx.toInt()
itemBinding.items.adapter = adapter
itemBinding.items.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
(activity as MainActivity).mainActive()
}
tvViewModel.changed()
}
}
}
}
})
fun nextSource() {
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)
val itemDecoration = context?.let { ItemDecoration(it) }
if (itemDecoration != null) {
itemBinding.items.addItemDecoration(itemDecoration)
}
if (SP.grid) {
itemBinding.items.layoutManager =
GridLayoutManager(context, 6)
itemBinding.items.layoutParams.height =
application.dp2Px(110 * ((tvListViewModelCurrent.size() + 6 - 1) / 6) + 5)
} else {
itemBinding.items.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
}
val layoutParams = itemBinding.row.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.topMargin = application.dp2Px(11)
itemBinding.row.layoutParams = layoutParams
itemBinding.row.setOnClickListener {
hideSelf()
}
itemBinding.items.addOnItemTouchListener(object : RecyclerView.OnItemTouchListener {
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
gestureDetector.onTouchEvent(e)
return false
}
tvViewModel.changed()
override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {
TODO("Not yet implemented")
}
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
TODO("Not yet implemented")
}
})
val layoutParamsHeader =
itemBinding.header.layoutParams as ViewGroup.MarginLayoutParams
layoutParamsHeader.topMargin = application.px2Px(itemBinding.header.marginTop)
layoutParamsHeader.bottomMargin = application.px2Px(itemBinding.header.marginBottom)
layoutParamsHeader.marginStart = application.px2Px(itemBinding.header.marginStart)
itemBinding.header.layoutParams = layoutParamsHeader
itemBinding.header.textSize = application.px2PxFont(itemBinding.header.textSize)
content.addView(itemBinding.row)
idx++
}
if (itemPosition >= tvListViewModel.size()) {
itemPosition = 0
}
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.tvListViewModel.value?.forEach { tvViewModel ->
tvViewModel.errInfo.observe(viewLifecycleOwner) { _ ->
if (tvViewModel.errInfo.value != null
&& tvViewModel.getTV().id == itemPosition
) {
if (tvViewModel.errInfo.value == "") {
(activity as? MainActivity)?.showPlayerFragment()
(activity as? MainActivity)?.hideErrorFragment()
(activity as? MainActivity)?.hideLoadingFragment()
} else {
(activity as? MainActivity)?.hidePlayerFragment()
(activity as? MainActivity)?.hideLoadingFragment()
(activity as? MainActivity)?.showErrorFragment(tvViewModel.errInfo.value.toString())
}
}
}
tvViewModel.ready.observe(viewLifecycleOwner) { _ ->
// not first time && channel not change
if (tvViewModel.ready.value != null
&& tvViewModel.getTV().id == itemPosition
&& check(tvViewModel)
) {
Log.i(TAG, "ready ${tvViewModel.getTV().title}")
(activity as? MainActivity)?.play(tvViewModel)
}
}
tvViewModel.change.observe(viewLifecycleOwner) { _ ->
if (tvViewModel.change.value != null) {
val title = tvViewModel.getTV().title
Log.i(TAG, "switch $title")
if (tvViewModel.getTV().pid != "") {
Log.i(TAG, "request $title")
lifecycleScope.launch(Dispatchers.IO) {
tvViewModel.let { Request.fetchData(it) }
}
(activity as? MainActivity)?.showInfoFragment(tvViewModel)
setPosition(
tvViewModel.getRowPosition(), tvViewModel.getItemPosition()
)
} else {
if (check(tvViewModel)) {
// TODO lastVideoUrl
(activity as? MainActivity)?.play(tvViewModel)
(activity as? MainActivity)?.showInfoFragment(tvViewModel)
setPosition(
tvViewModel.getRowPosition(), tvViewModel.getItemPosition()
)
}
}
}
}
}
(activity as MainActivity).fragmentReady(TAG)
}
}
fun changeMenu() {
if (SP.grid) {
for (i in rowList) {
if (i is RecyclerView) {
i.layoutManager = GridLayoutManager(context, 6)
i.layoutParams.height =
application.dp2Px(110 * (((i.adapter as CardAdapter).getItemCount() + 6 - 1) / 6) + 5)
}
}
} else {
for (i in rowList) {
if (i is RecyclerView) {
i.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
i.layoutParams.height = application.dp2Px(110 + 5)
}
}
}
}
private fun setupEventListeners() {
onItemViewClickedListener = ItemViewClickedListener()
onItemViewSelectedListener = ItemViewSelectedListener()
}
override fun onKey(keyCode: Int): Boolean {
if (this.isHidden) {
when (keyCode) {
KeyEvent.KEYCODE_DPAD_UP -> {
(activity as MainActivity).onKey(keyCode)
return true
}
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.getTV().id) {
itemPosition = item.getTV().id
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVViewModel(itemPosition)?.changed()
KeyEvent.KEYCODE_DPAD_DOWN -> {
(activity as MainActivity).onKey(keyCode)
return true
}
(activity as? MainActivity)?.switchMainFragment()
}
}
return false
}
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.getTV().id)
(activity as MainActivity).mainActive()
override fun onItemHasFocus(tvViewModel: TVViewModel) {
val row = tvViewModel.getRowPosition()
for (i in rowList) {
if (i.tag as Int != row) {
((i as RecyclerView).adapter as CardAdapter).focusable = false
(i.adapter as CardAdapter).clear()
} else {
((i as RecyclerView).adapter as CardAdapter).focusable = true
}
}
(activity as MainActivity).mainActive()
}
override fun onItemClicked(tvViewModel: TVViewModel) {
if (this.isHidden) {
(activity as? MainActivity)?.switchMainFragment()
return
}
if (itemPosition != tvViewModel.getTV().id) {
itemPosition = tvViewModel.getTV().id
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVViewModel(itemPosition)?.changed("menu")
Log.i(TAG, "onItemClicked ${tvViewModel.getTV().id}")
}
(activity as? MainActivity)?.switchMainFragment()
}
fun setPosition() {
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition) ?: return
val rowPosition = tvViewModel.getRowPosition()
val itemPosition = tvViewModel.getItemPosition()
setPosition(rowPosition, itemPosition)
}
fun setPosition(rowPosition: Int, itemPosition: Int) {
rowList[rowPosition].post {
when (val layoutManager = (rowList[rowPosition] as RecyclerView).layoutManager) {
is GridLayoutManager -> {
layoutManager.findViewByPosition(
itemPosition
)?.requestFocus()
}
is LinearLayoutManager -> {
layoutManager.findViewByPosition(
itemPosition
)?.requestFocus()
}
}
}
}
@@ -257,7 +348,7 @@ class MainFragment : BrowseSupportFragment() {
}
fun fragmentReady() {
tvListViewModel.getTVViewModel(itemPosition)?.changed()
tvListViewModel.getTVViewModel(itemPosition)?.changed("init")
tvListViewModel.tvListViewModel.value?.forEach { tvViewModel ->
updateEPG(tvViewModel)
@@ -269,7 +360,7 @@ class MainFragment : BrowseSupportFragment() {
if (itemPosition > -1 && itemPosition < tvListViewModel.size()) {
this.itemPosition = itemPosition
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVViewModel(itemPosition)?.changed()
tvListViewModel.getTVViewModel(itemPosition)?.changed("num")
} else {
Toast.makeText(context, "频道不存在", Toast.LENGTH_SHORT).show()
}
@@ -283,7 +374,7 @@ class MainFragment : BrowseSupportFragment() {
itemPosition = tvListViewModel.size() - 1
}
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVViewModel(itemPosition)?.changed()
tvListViewModel.getTVViewModel(itemPosition)?.changed("prev")
}
}
@@ -294,7 +385,7 @@ class MainFragment : BrowseSupportFragment() {
itemPosition = 0
}
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVViewModel(itemPosition)?.changed()
tvListViewModel.getTVViewModel(itemPosition)?.changed("next")
}
}
@@ -314,23 +405,51 @@ class MainFragment : BrowseSupportFragment() {
}
}
fun shouldHasFocus(tvModel: TVViewModel): Boolean {
return tvModel == tvListViewModel.getTVViewModel(itemPosition)
}
override fun onHiddenChanged(hidden: Boolean) {
Log.i(TAG, "onHiddenChanged $hidden")
super.onHiddenChanged(hidden)
if (!hidden) {
val tvModel = tvListViewModel.getTVViewModel(itemPosition)
val rowPosition = tvModel?.getRowPosition()
val itemPosition = tvModel?.getItemPosition()
Log.i(TAG, "toPosition $rowPosition $itemPosition")
for (i in rowList) {
if (i.tag as Int == rowPosition) {
((i as RecyclerView).adapter as CardAdapter).updateEPG()
(i.adapter as CardAdapter).focusable = true
(i.adapter as CardAdapter).toPosition(itemPosition!!)
break
}
}
} else {
view?.post {
for (i in rowList) {
((i as RecyclerView).adapter as CardAdapter).focusable = false
}
}
}
}
override fun onResume() {
Log.i(TAG, "onResume")
super.onResume()
}
override fun onStop() {
Log.i(TAG, "onStop")
super.onStop()
SP.itemPosition = itemPosition
Log.i(TAG, "$POSITION $itemPosition saved")
}
override fun onDestroy() {
Log.i(TAG, "onDestroy")
super.onDestroy()
}
override fun onDestroyView() {
Log.i(TAG, "onDestroyView")
super.onDestroyView()
_binding = null
}
companion object {
private const val TAG = "MainFragment"
private const val POSITION = "position"
@@ -1,23 +0,0 @@
package com.lizongying.mytv
import android.app.Application
import android.content.Context
import android.util.DisplayMetrics
import android.view.WindowManager
class MyApplication : Application() {
private lateinit var displayMetrics: DisplayMetrics
override fun onCreate() {
super.onCreate()
displayMetrics = DisplayMetrics()
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
// windowManager.defaultDisplay.getRealMetrics(displayMetrics)
windowManager.defaultDisplay.getMetrics(displayMetrics)
}
fun getDisplayMetrics(): DisplayMetrics {
return displayMetrics
}
}
@@ -0,0 +1,127 @@
package com.lizongying.mytv
import android.app.Application
import android.content.Context
import android.content.res.Resources
import android.util.DisplayMetrics
import android.view.WindowManager
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.multidex.MultiDex
import com.lizongying.mytv.models.MyViewModel
class MyTVApplication : Application() {
private lateinit var displayMetrics: DisplayMetrics
private lateinit var realDisplayMetrics: DisplayMetrics
private lateinit var windowManager: WindowManager
private var width = 0
private var height = 0
private var shouldWidth = 0
private var shouldHeight = 0
private var deviation = 0
private var ratio = 1.0
private var density = 2.0f
private var scale = 1.0f
lateinit var myViewModel: MyViewModel
override fun onCreate() {
super.onCreate()
displayMetrics = DisplayMetrics()
realDisplayMetrics = DisplayMetrics()
windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
windowManager.defaultDisplay.getMetrics(displayMetrics)
windowManager.defaultDisplay.getRealMetrics(realDisplayMetrics)
if (realDisplayMetrics.heightPixels > realDisplayMetrics.widthPixels) {
width = realDisplayMetrics.heightPixels
height = realDisplayMetrics.widthPixels
} else {
width = realDisplayMetrics.widthPixels
height = realDisplayMetrics.heightPixels
}
density = displayMetrics.density
ratio = if ((width.toDouble() / height) < (16.0 / 9)) {
width * 2 / 1920.0 / density
} else {
height * 2 / 1080.0 / density
}
if ((width.toDouble() / height) < (16.0 / 9.0)) {
ratio = width * 2 / 1920.0 / density
shouldWidth = width
shouldHeight = (width * 9.0 / 16.0).toInt()
} else {
ratio = height * 2 / 1080.0 / density
shouldHeight = height
shouldWidth = (height * 16.0 / 9.0).toInt()
}
deviation = width - Resources.getSystem().displayMetrics.widthPixels
scale = Resources.getSystem().displayMetrics.scaledDensity
myViewModel = ViewModelProvider(
ViewModelStore(),
ViewModelProvider.NewInstanceFactory()
)[MyViewModel::class.java]
}
fun getDisplayMetrics(): DisplayMetrics {
return displayMetrics
}
fun getRealDisplayMetrics(): DisplayMetrics {
return realDisplayMetrics
}
fun shouldWidthPx(): Int {
return shouldWidth
}
fun shouldHeightPx(): Int {
return shouldHeight
}
fun getWidth(): Int {
return width
}
fun getHeight(): Int {
return height
}
fun getDeviation(): Int {
return deviation
}
fun getRatio(): Double {
return ratio
}
fun dp2Px(dp: Int): Int {
return (dp * ratio * density + 0.5f).toInt()
}
fun px2Px(px: Int): Int {
return (px * ratio + 0.5f).toInt()
}
fun px2PxFont(px: Float): Float {
return (px * ratio / scale).toFloat()
}
fun sp2Px(sp: Float): Float {
return (sp * ratio * scale).toFloat()
}
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
MultiDex.install(base)
}
}
@@ -0,0 +1,6 @@
package com.lizongying.mytv
interface OnSharedPreferenceChangeListener {
fun onSharedPreferenceChanged(key: String)
}
@@ -3,8 +3,6 @@ package com.lizongying.mytv
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
@@ -17,45 +15,37 @@ import androidx.media3.common.VideoSize
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import com.google.android.exoplayer2.SimpleExoPlayer
import com.lizongying.mytv.databinding.PlayerBinding
import com.lizongying.mytv.models.TVViewModel
class PlayerFragment : Fragment(), SurfaceHolder.Callback {
class PlayerFragment : Fragment() {
private var _binding: PlayerBinding? = null
private var playerView: PlayerView? = null
private var tvViewModel: TVViewModel? = null
private val aspectRatio = 16f / 9f
private lateinit var surfaceView: SurfaceView
private lateinit var surfaceHolder: SurfaceHolder
private var exoPlayer: SimpleExoPlayer? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = PlayerBinding.inflate(inflater, container, false)
if (Utils.isTmallDevice()) {
_binding!!.playerView.visibility = View.GONE
surfaceView = _binding!!.surfaceView
surfaceHolder = surfaceView.holder
surfaceHolder.addCallback(this)
} else {
_binding!!.surfaceView.visibility = View.GONE
playerView = _binding!!.playerView
}
playerView = _binding!!.playerView
playerView?.viewTreeObserver?.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
@OptIn(UnstableApi::class)
override fun onGlobalLayout() {
playerView!!.viewTreeObserver.removeOnGlobalLayoutListener(this)
// val renderersFactory = context?.let { DefaultRenderersFactory(it) }
// renderersFactory?.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
playerView!!.player = activity?.let {
ExoPlayer.Builder(it)
// .setRenderersFactory(renderersFactory!!)
.build()
}
playerView!!.player?.playWhenReady = true
@@ -78,21 +68,22 @@ class PlayerFragment : Fragment(), SurfaceHolder.Callback {
override fun onPlayerError(error: PlaybackException) {
super.onPlayerError(error)
Log.e(TAG, "PlaybackException $error")
tvViewModel?.changed()
val err = "播放错误"
tvViewModel?.setErrInfo(err)
tvViewModel?.changed("retry")
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
if (isPlaying) {
(activity as MainActivity).isPlaying()
tvViewModel?.setErrInfo("")
}
}
})
}
})
(activity as MainActivity).fragmentReady("PlayerFragment")
(activity as MainActivity).fragmentReady(TAG)
return _binding!!.root
}
@@ -102,10 +93,7 @@ class PlayerFragment : Fragment(), SurfaceHolder.Callback {
playerView?.player?.run {
setMediaItem(MediaItem.fromUri(tvViewModel.getVideoUrlCurrent()))
prepare()
}
exoPlayer?.run {
setMediaItem(com.google.android.exoplayer2.MediaItem.fromUri(tvViewModel.getVideoUrlCurrent()))
prepare()
volume = tvViewModel.getTV().volume
}
}
@@ -117,11 +105,6 @@ class PlayerFragment : Fragment(), SurfaceHolder.Callback {
playerView!!.player?.prepare()
playerView!!.player?.play()
}
if (exoPlayer?.isPlaying == false) {
Log.i(TAG, "replay")
exoPlayer?.prepare()
exoPlayer?.play()
}
}
override fun onResume() {
@@ -134,9 +117,6 @@ class PlayerFragment : Fragment(), SurfaceHolder.Callback {
if (playerView != null && playerView!!.player?.isPlaying == true) {
playerView!!.player?.stop()
}
if (exoPlayer?.isPlaying == true) {
exoPlayer?.stop()
}
}
override fun onDestroy() {
@@ -144,7 +124,6 @@ class PlayerFragment : Fragment(), SurfaceHolder.Callback {
if (playerView != null) {
playerView!!.player?.release()
}
exoPlayer?.release()
}
override fun onDestroyView() {
@@ -153,18 +132,6 @@ class PlayerFragment : Fragment(), SurfaceHolder.Callback {
}
companion object {
private const val TAG = "PlaybackVideoFragment"
}
override fun surfaceCreated(holder: SurfaceHolder) {
exoPlayer = SimpleExoPlayer.Builder(requireContext()).build()
exoPlayer?.setVideoSurfaceHolder(surfaceHolder)
exoPlayer?.playWhenReady = true
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
private const val TAG = "PlayerFragment"
}
}
+20 -4
View File
@@ -13,12 +13,12 @@ object SP {
// If use channel num to select channel or not
private const val KEY_CHANNEL_NUM = "channel_num"
private const val KEY_TIME = "time"
const val KEY_TIME = "time"
// If start app on device boot or not
private const val KEY_BOOT_STARTUP = "boot_startup"
private const val KEY_GRID = "grid"
const val KEY_GRID = "grid"
// Position in list of the selected channel item
private const val KEY_POSITION = "position"
@@ -28,6 +28,8 @@ object SP {
private lateinit var sp: SharedPreferences
private var listener: OnSharedPreferenceChangeListener? = null
/**
* The method must be invoked as early as possible(At least before using the keys)
*/
@@ -35,6 +37,10 @@ object SP {
sp = context.getSharedPreferences(SP_FILE_NAME, Context.MODE_PRIVATE)
}
fun setOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {
this.listener = listener
}
var channelReversal: Boolean
get() = sp.getBoolean(KEY_CHANNEL_REVERSAL, false)
set(value) = sp.edit().putBoolean(KEY_CHANNEL_REVERSAL, value).apply()
@@ -45,7 +51,12 @@ object SP {
var time: Boolean
get() = sp.getBoolean(KEY_TIME, true)
set(value) = sp.edit().putBoolean(KEY_TIME, value).apply()
set(value) {
if (value != this.time) {
sp.edit().putBoolean(KEY_TIME, value).apply()
listener?.onSharedPreferenceChanged(KEY_TIME)
}
}
var bootStartup: Boolean
get() = sp.getBoolean(KEY_BOOT_STARTUP, false)
@@ -53,7 +64,12 @@ object SP {
var grid: Boolean
get() = sp.getBoolean(KEY_GRID, false)
set(value) = sp.edit().putBoolean(KEY_GRID, value).apply()
set(value) {
if (value != this.grid) {
sp.edit().putBoolean(KEY_GRID, value).apply()
listener?.onSharedPreferenceChanged(KEY_GRID)
}
}
var itemPosition: Int
get() = sp.getInt(KEY_POSITION, 0)
@@ -1,11 +1,23 @@
package com.lizongying.mytv
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.marginEnd
import androidx.core.view.marginTop
import androidx.fragment.app.DialogFragment
import com.lizongying.mytv.api.YSP
import com.lizongying.mytv.databinding.SettingBinding
@@ -21,6 +33,11 @@ class SettingFragment : DialogFragment() {
dialog?.window?.apply {
addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
setAttributes(attributes)
}
}
}
@@ -71,35 +88,156 @@ class SettingFragment : DialogFragment() {
}
}
updateManager = UpdateManager(context, this, context.appVersionCode)
binding.checkVersion.setOnClickListener(
OnClickListenerCheckVersion(
activity as MainActivity,
updateManager
)
)
binding.switchGrid.run {
isChecked = SP.grid
setOnCheckedChangeListener { _, isChecked ->
SP.grid = isChecked
(activity as MainActivity).settingDelayHide()
}
}
binding.exit.setOnClickListener{
binding.checkVersion.setOnClickListener {
(activity as MainActivity).settingDelayHide()
requestInstallPermissions()
}
binding.clear.setOnClickListener {
(requireActivity() as MainActivity).syncTime()
SP.guid = ""
YSP.getGuid()
}
val application = requireActivity().applicationContext as MyTVApplication
val textSize = application.px2PxFont(binding.switchChannelReversal.textSize)
binding.content.layoutParams.width =
application.px2Px(binding.content.layoutParams.width)
binding.content.setPadding(
application.px2Px(binding.content.paddingLeft),
application.px2Px(binding.content.paddingTop),
application.px2Px(binding.content.paddingRight),
application.px2Px(binding.content.paddingBottom)
)
binding.name.textSize = application.px2PxFont(binding.name.textSize)
binding.version.textSize = textSize
val layoutParamsVersion = binding.version.layoutParams as ViewGroup.MarginLayoutParams
layoutParamsVersion.topMargin = application.px2Px(binding.version.marginTop)
binding.version.layoutParams = layoutParamsVersion
binding.checkVersion.textSize = textSize
val layoutParamsCheckVersion =
binding.checkVersion.layoutParams as ViewGroup.MarginLayoutParams
layoutParamsCheckVersion.marginEnd = application.px2Px(binding.checkVersion.marginEnd)
binding.checkVersion.layoutParams = layoutParamsCheckVersion
binding.versionName.textSize = textSize
binding.clear.textSize = textSize
binding.exit.textSize = textSize
val layoutParamsChannelSwitch =
binding.switchChannelReversal.layoutParams as ViewGroup.MarginLayoutParams
layoutParamsChannelSwitch.topMargin =
application.px2Px(binding.switchChannelReversal.marginTop)
binding.switchChannelReversal.textSize = textSize
binding.switchChannelReversal.layoutParams = layoutParamsChannelSwitch
binding.switchChannelNum.textSize = textSize
binding.switchChannelNum.layoutParams = layoutParamsChannelSwitch
binding.switchTime.textSize = textSize
binding.switchTime.layoutParams = layoutParamsChannelSwitch
binding.switchBootStartup.textSize = textSize
binding.switchBootStartup.layoutParams = layoutParamsChannelSwitch
binding.switchGrid.textSize = textSize
binding.switchGrid.layoutParams = layoutParamsChannelSwitch
binding.appreciate.layoutParams.width =
application.px2Px(binding.appreciate.layoutParams.width)
binding.exit.setOnClickListener {
requireActivity().finishAffinity()
}
// val myViewModel = application.myViewModel
// application.myViewModel.downloadProgress.observe(viewLifecycleOwner) { _ ->
// val downloadProgress = myViewModel.downloadProgress.value
// if (downloadProgress != null) {
// if (downloadProgress == 100) {
// binding.progressBar.visibility = GONE
// } else {
// if (!binding.progressBar.isVisible) {
// binding.progressBar.visibility = VISIBLE
// }
// binding.progressBar.progress = downloadProgress
// }
// }
// }
updateManager = UpdateManager(context, context.appVersionCode)
return binding.root
}
fun setVersionName(versionName: String) {
if (_binding != null) {
binding.versionName.text = versionName
private fun requestInstallPermissions() {
val context = requireContext()
val permissionsList: MutableList<String> = ArrayList()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !context.packageManager.canRequestPackageInstalls()) {
permissionsList.add(Manifest.permission.REQUEST_INSTALL_PACKAGES)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
permissionsList.add(Manifest.permission.READ_EXTERNAL_STORAGE)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
ContextCompat.checkSelfPermission(
context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
permissionsList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
if (permissionsList.isNotEmpty()) {
ActivityCompat.requestPermissions(
requireActivity(),
permissionsList.toTypedArray<String>(),
PERMISSIONS_REQUEST_CODE
)
} else {
updateManager.checkAndUpdate()
}
}
internal class OnClickListenerCheckVersion(
private val mainActivity: MainActivity,
private val updateManager: UpdateManager
) :
View.OnClickListener {
override fun onClick(view: View?) {
mainActivity.settingDelayHide()
updateManager.checkAndUpdate()
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSIONS_REQUEST_CODE) {
var allPermissionsGranted = true
for (result in grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allPermissionsGranted = false
break
}
}
if (allPermissionsGranted) {
updateManager.checkAndUpdate()
} else {
Toast.makeText(context, "权限授权失败", Toast.LENGTH_LONG).show()
}
}
}
@@ -110,6 +248,7 @@ class SettingFragment : DialogFragment() {
companion object {
const val TAG = "SettingFragment"
const val PERMISSIONS_REQUEST_CODE = 1
}
}
@@ -2,10 +2,11 @@ package com.lizongying.mytv
import android.os.Bundle
import android.os.Handler
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.marginEnd
import androidx.core.view.marginTop
import androidx.fragment.app.Fragment
import com.lizongying.mytv.Utils.getDateFormat
import com.lizongying.mytv.databinding.TimeBinding
@@ -21,74 +22,57 @@ class TimeFragment : Fragment() {
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
Log.i(TAG, "onCreateView")
_binding = TimeBinding.inflate(inflater, container, false)
val activity = requireActivity()
val application = activity.applicationContext as MyApplication
val displayMetrics = application.getDisplayMetrics()
val application = requireActivity().applicationContext as MyTVApplication
displayMetrics.density
binding.time.layoutParams.width = application.px2Px(binding.time.layoutParams.width)
binding.time.layoutParams.height = application.px2Px(binding.time.layoutParams.height)
var screenWidth = displayMetrics.widthPixels
var screenHeight = displayMetrics.heightPixels
if (screenHeight > screenWidth) {
screenWidth = displayMetrics.heightPixels
screenHeight = displayMetrics.widthPixels
val layoutParams = binding.time.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.topMargin = application.px2Px(binding.time.marginTop)
layoutParams.marginEnd = application.px2Px(binding.time.marginEnd)
binding.time.layoutParams = layoutParams
binding.content.textSize = application.px2PxFont(binding.content.textSize)
binding.channel.textSize = application.px2PxFont(binding.channel.textSize)
binding.main.layoutParams.width = application.shouldWidthPx()
binding.main.layoutParams.height = application.shouldHeightPx()
if (SP.time) {
handler.removeCallbacks(showRunnable)
handler.postDelayed(showRunnable, 0)
} else {
handler.removeCallbacks(showRunnable)
}
val ratio = 16f / 9f
if (screenWidth / screenHeight > ratio) {
val x = ((screenWidth - screenHeight * ratio) / 2).toInt()
val originalLayoutParams = binding.time.layoutParams as ViewGroup.MarginLayoutParams
originalLayoutParams.rightMargin += x
binding.time.layoutParams = originalLayoutParams
}
if (screenWidth / screenHeight < ratio) {
val y = ((screenHeight - screenWidth / ratio) / 2).toInt()
val originalLayoutParams = binding.time.layoutParams as ViewGroup.MarginLayoutParams
originalLayoutParams.topMargin += y
binding.time.layoutParams = originalLayoutParams
}
(activity as MainActivity).fragmentReady("TimeFragment")
(activity as MainActivity).fragmentReady(TAG)
return binding.root
}
override fun onResume() {
super.onResume()
if (view?.visibility == View.VISIBLE) {
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if (!hidden) {
handler.removeCallbacks(showRunnable)
handler.postDelayed(showRunnable, 0)
} else {
handler.removeCallbacks(showRunnable)
}
}
override fun onPause() {
super.onPause()
handler.removeCallbacks(showRunnable)
}
private val showRunnable: Runnable = Runnable {
run {
if (_binding == null) {
return@Runnable
}
binding.content.text = getDateFormat("HH:mm")
handler.postDelayed(showRunnable, delay)
}
}
fun show() {
view?.visibility = View.VISIBLE
handler.removeCallbacks(showRunnable)
handler.postDelayed(showRunnable, 0)
}
fun hide() {
view?.visibility = View.GONE
handler.removeCallbacks(showRunnable)
}
override fun onDestroyView() {
handler.removeCallbacks(showRunnable)
super.onDestroyView()
_binding = null
}
@@ -1,206 +1,73 @@
package com.lizongying.mytv
import android.app.Activity
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 androidx.core.app.ActivityCompat
import androidx.core.content.PermissionChecker
import androidx.core.content.PermissionChecker.checkSelfPermission
import com.lizongying.mytv.api.ReleaseV2
import com.lizongying.mytv.requests.MyRequest
import androidx.fragment.app.FragmentActivity
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import com.lizongying.mytv.api.ApiClient
import com.lizongying.mytv.requests.ReleaseRequest
import com.lizongying.mytv.requests.ReleaseResponse
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
) :
ConfirmationFragment.ConfirmationListener {
private var myRequest = MyRequest()
private var release: ReleaseV2? = null
private var downloadReceiver: DownloadReceiver? = null
private var releaseRequest = ReleaseRequest()
private var release: ReleaseResponse? = null
fun checkAndUpdate() {
if (!haveStoragePermission()) {
return
}
CoroutineScope(Dispatchers.Main).launch {
var text = "版本获取失败"
var update = false
try {
release = myRequest.getRelease()
Log.i(TAG, "versionCode $versionCode ${release?.c}")
if (release?.c != null) {
text = if (release?.c!! > versionCode) {
"最新版本:${release?.n}\n${release?.d ?: ""}"
release = releaseRequest.getRelease()
val code = release?.version_code
if (code != null) {
if (code.toLong() >= versionCode) {
text = "最新版本:${release?.version_name}"
update = true
} else {
"已是最新版本,不需要更新"
text = "已是最新版本,不需要更新"
}
}
} catch (e: Exception) {
Log.e(TAG, "Error occurred: ${e.message}", e)
}
updateUI(text)
updateUI(text, update)
}
}
private fun updateUI(text: String) {
val dialog = ConfirmationFragment(this@UpdateManager, text)
dialog.show(settingFragment.fragmentManager, TAG)
private fun updateUI(text: String, update: Boolean) {
val dialog = ConfirmationFragment(this@UpdateManager, text, update)
dialog.show((context as FragmentActivity).supportFragmentManager, TAG)
}
private fun haveStoragePermission(): Boolean {
if (Build.VERSION.SDK_INT >= 23) {
if (checkSelfPermission(context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
=== PermissionChecker.PERMISSION_GRANTED
) {
Log.e("Permission error", "You have permission")
return true
} else {
Log.e("Permission error", "You have asked for permission")
ActivityCompat.requestPermissions(
context as Activity, arrayOf(
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
), 1
)
return false
}
} else { //you don't need to worry about these stuff below api level 23
Log.e("Permission error", "You already have the permission")
return true
}
}
private fun startDownload(release: ReleaseV2) {
val apkFileName = "my-tv-${release.n}.apk"
Log.i(TAG, "apkFileName $apkFileName")
val downloadManager =
context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = Request(Uri.parse(release.u))
Log.i(TAG, "url ${Uri.parse(release.u)}")
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.mkdirs()
request.setDestinationInExternalFilesDir(
context,
Environment.DIRECTORY_DOWNLOADS,
apkFileName
)
request.setTitle("${settingFragment.resources.getString(R.string.app_name)} ${release.n}")
request.setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
request.setAllowedOverRoaming(false)
request.setMimeType("application/vnd.android.package-archive")
// 获取下载任务的引用
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
private fun startDownload(context: Context, release: ReleaseResponse) {
val versionName = release.version_name
val apkName = "my-tv"
val apkFileName = "$apkName-${release.version_name}.apk"
val downloadUrl =
"${ApiClient.DOWNLOAD_HOST}${release.version_name}/$apkName-${release.version_name}.apk"
Log.i(TAG, "downloadUrl: $downloadUrl")
val downloadRequest = OneTimeWorkRequestBuilder<UpdateWorker>()
.setInputData(
workDataOf(
"VERSION_NAME" to versionName,
"APK_FILENAME" to apkFileName,
"DOWNLOAD_URL" to downloadUrl
)
)
installIntent.setDataAndType(apkUri, "application/vnd.android.package-archive")
installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(installIntent)
}
.build()
WorkManager.getInstance(context).enqueue(downloadRequest)
}
companion object {
@@ -209,16 +76,9 @@ class UpdateManager(
override fun onConfirm() {
Log.i(TAG, "onConfirm $release")
release?.let { startDownload(it) }
release?.let { startDownload(context, it) }
}
override fun onCancel() {
}
fun destroy() {
if (downloadReceiver != null) {
context.unregisterReceiver(downloadReceiver)
Log.i(TAG, "destroy downloadReceiver")
}
}
}
@@ -0,0 +1,158 @@
package com.lizongying.mytv
import android.app.DownloadManager
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.content.FileProvider
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.lizongying.mytv.models.MyViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
class UpdateWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
private lateinit var notificationManager: NotificationManager
private lateinit var notificationBuilder: NotificationCompat.Builder
private lateinit var myViewModel: MyViewModel
override suspend fun doWork(): Result {
val downloadUrl = inputData.getString("DOWNLOAD_URL") ?: return Result.failure()
val apkFileName = inputData.getString("APK_FILENAME") ?: return Result.failure()
val versionName = inputData.getString("VERSION_NAME") ?: return Result.failure()
showNotification(applicationContext)
val app = applicationContext as MyTVApplication
myViewModel = app.myViewModel
return withContext(Dispatchers.IO) {
try {
downloadAndInstall(applicationContext, downloadUrl, apkFileName, versionName)
Result.success()
} catch (e: Exception) {
Result.failure()
}
}
}
private fun showNotification(context: Context) {
notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationBuilder = NotificationCompat.Builder(context, "download_channel").apply {
setSmallIcon(android.R.drawable.stat_sys_download)
setContentTitle("Downloading Update")
setContentText("Download in progress")
priority = NotificationCompat.PRIORITY_LOW
setOngoing(true)
setProgress(100, 0, false)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
"download_channel",
"Download Updates",
NotificationManager.IMPORTANCE_LOW
)
notificationManager.createNotificationChannel(channel)
}
notificationManager.notify(1, notificationBuilder.build())
}
private fun downloadAndInstall(
context: Context,
downloadUrl: String,
apkFileName: String,
versionName: String
) {
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.mkdirs()
Log.i(TAG, "save dir ${Environment.DIRECTORY_DOWNLOADS}")
val request = DownloadManager.Request(Uri.parse(downloadUrl)).apply {
setTitle("${context.resources.getString(R.string.app_name)} $versionName Downloading")
setDescription("Downloading the latest version of the app")
setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, apkFileName)
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
// setAllowedOverRoaming(false)
setMimeType("application/vnd.android.package-archive")
}
val manager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val downloadId = manager.enqueue(request)
var downloading = true
while (downloading) {
val query = DownloadManager.Query().setFilterById(downloadId)
val cursor = manager.query(query)
if (cursor.moveToFirst()) {
val bytesDownloaded =
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
val bytesTotal =
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
if (bytesTotal > 0) {
val progress = (bytesDownloaded * 100L / bytesTotal).toInt()
Log.i(TAG, "progress $progress")
// myViewModel.setDownloadProgress(progress)
notificationBuilder.setProgress(100, progress, false)
notificationManager.notify(1, notificationBuilder.build())
}
when (cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))) {
DownloadManager.STATUS_SUCCESSFUL -> downloading = false
DownloadManager.STATUS_FAILED -> {
downloading = false
throw Exception("Download failed")
}
}
}
cursor.close()
}
notificationBuilder.setContentText("Download complete")
.setProgress(0, 0, false)
.setOngoing(false)
notificationManager.notify(1, notificationBuilder.build())
val apkFile = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
apkFileName
)
if (apkFile.exists()) {
val apkUri: Uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(context, context.packageName + ".fileprovider", apkFile)
.apply {
Intent.FLAG_GRANT_READ_URI_PERMISSION
}
} else {
Uri.parse("file://${apkFile.absolutePath}")
// Uri.fromFile(apkFile)
}
// val apkUri = Uri.parse("file://$apkFile")
Log.i(TAG, "apkUri $apkUri")
val installIntent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(apkUri, "application/vnd.android.package-archive")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(installIntent)
} else {
Log.e(TAG, "APK file does not exist!")
}
}
companion object {
private const val TAG = "UpdateWorker"
}
}
+174 -6
View File
@@ -2,11 +2,15 @@ package com.lizongying.mytv
import android.content.res.Resources
import android.os.Build
import android.util.Log
import android.util.TypedValue
import com.google.gson.Gson
import com.lizongying.mytv.api.TimeResponse
import com.lizongying.mytv.requests.Request
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
@@ -15,6 +19,12 @@ import java.util.Locale
object Utils {
private var between: Long = 0
private var a: String = ""
var b: String = ""
private var c: String = ""
private var listener: Request.RequestListener? = null
fun getDateFormat(format: String): String {
return SimpleDateFormat(
format,
@@ -31,13 +41,57 @@ object Utils {
}
suspend fun init() {
var currentTimeMillis: Long = 0
try {
currentTimeMillis = getTimestampFromServer()
val currentTimeMillis = getTimestampFromServer()
if (currentTimeMillis > 0) {
between = System.currentTimeMillis() - currentTimeMillis
}
} catch (e: Exception) {
println("Failed to retrieve timestamp from server: ${e.message}")
}
between = System.currentTimeMillis() - currentTimeMillis
var x = ""
try {
x = getNothing()
} catch (e: Exception) {
x = ""
println("a ${e.message}")
}
if (x != "") {
try {
x = getNothing2(x)
} catch (e: Exception) {
x = ""
println("b ${e.message}")
}
}
// if (x != "") {
// try {
// getNothing3(x)
// } catch (e: Exception) {
// println("b ${e.message}")
// }
// }
withContext(Dispatchers.Main) {
listener?.onRequestFinished(null)
}
}
class RetryInterceptor(private val maxRetry: Int) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var response = chain.proceed(request)
var tryCount = 0
while (!response.isSuccessful && tryCount < maxRetry) {
tryCount++
response = chain.proceed(request)
}
return response
}
}
/**
@@ -47,8 +101,8 @@ object Utils {
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()
.addInterceptor(RetryInterceptor(3))
.build()
val request = okhttp3.Request.Builder()
.url("https://api.m.taobao.com/rest/api3.do?api=mtop.common.getTimestamp")
.build()
@@ -59,7 +113,115 @@ object Utils {
Gson().fromJson(string, TimeResponse::class.java).data.t.toLong()
}
} catch (e: IOException) {
// Handle network errors
throw IOException("Error during network request", e)
}
}
}
private val regex = Regex("""chunk-vendors\.([^.]+)\.js""")
private suspend fun getNothing(): String {
return withContext(Dispatchers.IO) {
val client = okhttp3.OkHttpClient.Builder()
.addInterceptor(RetryInterceptor(3))
.build()
val request = okhttp3.Request.Builder()
.url("https://www.yangshipin.cn")
.build()
try {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val string = response.body()?.string()
val matchResult = string?.let { regex.find(it) }
var x = ""
if (matchResult != null) {
x = matchResult.groupValues[1]
}
x
}
} catch (e: IOException) {
throw IOException("Error during network request", e)
}
}
}
private val regex2 = Regex(""""ysp_tx"[^:]+:"([^"]+)[^:]+:"([^"]+)""")
private val regex3 = Regex(""""(https[^"]+wasm([^"]+))""")
private suspend fun getNothing2(x: String): String {
return withContext(Dispatchers.IO) {
val client = okhttp3.OkHttpClient.Builder()
.addInterceptor(RetryInterceptor(3))
.build()
val request = okhttp3.Request.Builder()
.url("https://www.yangshipin.cn/js/chunk-vendors.$x.js")
.build()
try {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val string = response.body()?.string()
val matches = string?.let { regex2.findAll(it) }
var matchResult = matches?.lastOrNull()
if (matchResult != null) {
val (aa, bb) = matchResult.destructured
a = aa
b = bb
}
matchResult = string?.let { regex3.find(it) }
if (matchResult != null) {
val (cc) = matchResult.destructured
c = cc
Log.i("", "ccccc $c")
}
c
}
} catch (e: IOException) {
throw IOException("Error during network request", e)
}
}
}
private suspend fun getNothing3(x: String): String {
return withContext(Dispatchers.IO) {
val client = okhttp3.OkHttpClient.Builder()
.addInterceptor(RetryInterceptor(3))
.build()
val request = okhttp3.Request.Builder()
.url("https://www.yangshipin.cn/js/chunk-vendors.$x.js")
.build()
try {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val string = response.body()?.string()
val matches = string?.let { regex2.findAll(it) }
var matchResult = matches?.lastOrNull()
if (matchResult != null) {
val (aa, bb) = matchResult.destructured
a = aa
b = bb
}
matchResult = string?.let { regex3.find(it) }
if (matchResult != null) {
val (cc) = matchResult.destructured
c = cc
}
c
}
} catch (e: IOException) {
throw IOException("Error during network request", e)
}
}
@@ -72,6 +234,8 @@ object Utils {
}
fun dpToPx(dp: Int): Int {
// val density = Resources.getSystem().displayMetrics.density
// return (dp * density + 0.5f).toInt()
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), Resources.getSystem().displayMetrics
).toInt()
@@ -88,4 +252,8 @@ object Utils {
}
fun isTmallDevice() = Build.MANUFACTURER.equals("Tmall", ignoreCase = true)
fun setRequestListener(listener: Request.RequestListener) {
this.listener = listener
}
}
@@ -3,7 +3,9 @@ package com.lizongying.mytv.api
import android.os.Build
import android.util.Log
import com.google.gson.Gson
import com.lizongying.mytv.jce.JceConverterFactory
import com.lizongying.mytv.requests.ReleaseService
import okhttp3.ConnectionSpec
import okhttp3.OkHttpClient
import okhttp3.TlsVersion
@@ -23,17 +25,19 @@ class ApiClient {
private val protoUrl = "https://capi.yangshipin.cn/"
private val traceUrl = "https://btrace.yangshipin.cn/"
private val trace2Url = "https://aatc-api.yangshipin.cn/"
private val tokenUrl = "https://h5access.yangshipin.cn/"
private val trace3Url = "https://dtrace.ysp.cctv.cn/"
private val jceUrl = "https://jacc.ysp.cctv.cn/"
private val fUrl = "https://m.fengshows.com/"
private var okHttpClient = getUnsafeOkHttpClient()
private var gson = Gson()
val yspApiService: YSPApiService by lazy {
Retrofit.Builder()
.baseUrl(yspUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build().create(YSPApiService::class.java)
}
@@ -41,15 +45,15 @@ class ApiClient {
Retrofit.Builder()
.baseUrl(myUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build().create(YSPTokenService::class.java)
}
val releaseService: ReleaseService by lazy {
Retrofit.Builder()
.baseUrl(myUrl)
.baseUrl(HOST)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build().create(ReleaseService::class.java)
}
@@ -65,7 +69,7 @@ class ApiClient {
Retrofit.Builder()
.baseUrl(traceUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build().create(YSPBtraceService::class.java)
}
@@ -73,15 +77,23 @@ class ApiClient {
Retrofit.Builder()
.baseUrl(trace2Url)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build().create(YSPBtraceService::class.java)
}
val yspToken2Service: YSPApiService by lazy {
Retrofit.Builder()
.baseUrl(tokenUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build().create(YSPApiService::class.java)
}
val yspBtraceService3: YSPBtraceService by lazy {
Retrofit.Builder()
.baseUrl(trace3Url)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build().create(YSPBtraceService::class.java)
}
@@ -97,7 +109,7 @@ class ApiClient {
Retrofit.Builder()
.baseUrl(fUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build().create(FAuthService::class.java)
}
@@ -170,4 +182,9 @@ class ApiClient {
throw RuntimeException(e)
}
}
companion object {
const val HOST = "https://gitee.com/lizongying/my-tv/raw/"
const val DOWNLOAD_HOST = "https://gitee.com/lizongying/my-tv/releases/download/"
}
}
@@ -1,4 +1,4 @@
package com.lizongying.mytv
package com.lizongying.mytv.api
import android.content.Context
@@ -7,10 +7,14 @@ class Encryptor {
external fun encrypt(t: String, e: String, r: String, n: String, i: String): String
external fun encrypt2(t: String): String
external fun hash(data: ByteArray): ByteArray?
external fun hash2(data: ByteArray): ByteArray?
external fun hash3(data: ByteArray): ByteArray?
companion object {
init {
System.loadLibrary("native")
@@ -1,17 +1,17 @@
package com.lizongying.mytv.api
data class KvcollectRequest(
val BossId: Int = 2727,
val Pwd: Int = 1424084450,
val BossId: Int = 99150,
val Pwd: Int = 1999332929,
val cdn: String = "waibao",
val cmd: Int = 263,
val defn: String = "fhd",
val downspeed: Double = 10.0,
val downspeed: Int = 10,
val durl: String = "", //
val errcode: String = "",
val fact1: String = "ysp_pc_live_b",
val firstreport: Int = 0,
val fplayerver: String = "100",
val fplayerver: String = "180",
val ftime: String = "", //
val geturltime: Int = 0,
val guid: String = "", //
@@ -39,17 +39,17 @@ data class KvcollectRequest(
data class KvcollectRequest2(
val BossId: Int = 2727,
val Pwd: Int = 1424084450,
val BossId: Int = 99150,
val Pwd: Int = 1999332929,
val cdn: String = "waibao",
val cmd: Int = 263,
val defn: String = "fhd",
val downspeed: String = "10",
val downspeed: Int = 10,
val durl: String = "", //
val errcode: String = "",
val fact1: String = "ysp_pc_live_b",
val firstreport: Int = 0,
val fplayerver: String = "100",
val fplayerver: String = "180",
val ftime: String = "", //
val geturltime: Int = 0,
val guid: String = "", //
@@ -68,9 +68,9 @@ data class KvcollectRequest2(
val sRef: String = "",
val sUrl: String = "", //
val sdtfrom: String = "ysp_pc_01",
val seq: Int = 9, //
val seq: Int = 0, //
var signature: String = "",
val url: String = "", //
val viewid: String = "", //
val _dc: Double = 0.0,
val _dc: String = "",
)
@@ -1,12 +0,0 @@
package com.lizongying.mytv.api
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path
interface ReleaseService {
@GET("my-tv/v2/release/{name}")
fun getRelease(
@Path("name") date: String = "1",
): Call<ReleaseV2>
}
@@ -0,0 +1,11 @@
package com.lizongying.mytv.api
data class Token2(
val ret: Int?,
val msg: String?,
val data: Data,
) {
data class Data(
val token: String,
)
}
@@ -1,10 +1,12 @@
package com.lizongying.mytv.api
import android.content.Context
import com.lizongying.mytv.Encryptor
import android.util.Log
import com.lizongying.mytv.SP
import com.lizongying.mytv.Utils
import com.lizongying.mytv.Utils.getDateTimestamp
import com.lizongying.mytv.models.TVViewModel
import java.security.MessageDigest
import kotlin.math.floor
import kotlin.random.Random
@@ -53,6 +55,14 @@ object YSP {
private var appid = "ysp_pc"
var token = ""
var token2 = ""
var yspsdkinput = ""
var openapi_signature = ""
var yspticket = ""
var nseqId = 1
var nrequest_id = ""
private var encryptor = Encryptor()
@@ -76,10 +86,45 @@ object YSP {
cKey =
encryptor.encrypt(cnlid, timeStr, appVer, guid, platform)
randStr = getRand()
signature = getSignature()
yspticket = encryptor.encrypt2("$livepid&$timeStr&$guid&519748109").lowercase()
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 getTokenData(tvModel: TVViewModel) {
livepid = tvModel.getTV().pid
cnlid = tvModel.getTV().sid
defn = "fhd"
randStr = getRand()
if (tvModel.retryTimes > 0) {
guid = newGuid()
}
cKey =
encryptor.encrypt(cnlid, timeStr, appVer, guid, platform)
yspsdkinput =
md("adjust=$adjust&app_version=$appVersion&appVer=$appVer&channel=$channel&cKey=$cKey&cmd=$cmd&cnlid=$cnlid&defn=$defn&devid=$devid&dtype=$dtype&encryptVer=$encryptVer&guid=$guid&livepid=$livepid&otype=$otype&platform=$platform&sphttps=$sphttps&stream=$stream")
nseqId++
nrequest_id = "999999" + getRand() + getTimeStr()
openapi_signature =
md("yspappid:519748109;host:www.yangshipin.cn;protocol:https:;token:$token2;input:$yspsdkinput-$guid-$nseqId-$nrequest_id;")
}
fun md(str: String): String {
val md = MessageDigest.getInstance("MD5")
md.update(str.toByteArray())
val digest = md.digest()
return digest.let { it -> it.joinToString("") { "%02x".format(it) } }
}
fun getAuthData(tvModel: TVViewModel): String {
livepid = tvModel.getTV().pid
@@ -93,7 +138,7 @@ object YSP {
return """pid=$livepid&guid=$guid&appid=$appid&rand_str=$randStr&signature=$signature"""
}
private fun getTimeStr(): String {
fun getTimeStr(): String {
return getDateTimestamp().toString()
}
@@ -107,6 +152,7 @@ object YSP {
var guid = SP.guid
if (guid.length < 18) {
guid = generateGuid()
this.guid = guid
SP.guid = guid
}
return guid
@@ -137,13 +183,13 @@ object YSP {
private fun getAuthSignature(): String {
val e =
"appid=${appid}&guid=${guid}&pid=${livepid}&rand_str=${randStr}".toByteArray()
val hashedData = encryptor.hash2(e) ?: return ""
"appid=${appid}&guid=${guid}&pid=${livepid}&rand_str=${randStr}${Utils.b}".toByteArray()
val hashedData = encryptor.hash3(e) ?: return ""
return hashedData.let { it -> it.joinToString("") { "%02x".format(it) } }
}
fun getAuthSignature(e: String): String {
val hashedData = encryptor.hash2(e.toByteArray()) ?: return ""
val hashedData = encryptor.hash3("$e${Utils.b}".toByteArray()) ?: return ""
return hashedData.let { it -> it.joinToString("") { "%02x".format(it) } }
}
}
@@ -2,9 +2,11 @@ package com.lizongying.mytv.api
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.Query
interface YSPApiService {
@@ -18,6 +20,12 @@ interface YSPApiService {
fun getLiveInfo(
@Header("cookie") cookie: String,
@Header("Yspplayertoken") token: String,
@Header("Yspsdkinput") Yspsdkinput: String,
@Header("yspsdksign") yspsdksign: String,
@Header("Yspticket") Yspticket: String,
@Header("Seqid") Seqid: String,
@Header("Request-Id") RequestId: String,
@Header("Yspappid") Yspappid: String="519748109",
@Body request: LiveInfoRequest,
): Call<LiveInfo>
@@ -32,4 +40,20 @@ interface YSPApiService {
@Header("cookie") cookie: String,
@Body request: AuthRequest,
): Call<Auth>
@GET("web/open/token")
@Headers(
"content-type: application/json",
"referer: https://www.yangshipin.cn/",
"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 getToken(
@Header("cookie") cookie: String,
@Query("yspappid") yspappid: String = "519748109",
@Query("guid") guid: String = "",
@Query("vappid") vappid: String = "59306155",
@Query("vsecret") vsecret: String = "b42702bf7309a179d102f3d51b1add2fda0bc7ada64cb801",
@Query("raw") raw: String = "1",
@Query("ts") ts: String = "",
): Call<Token2>
}
@@ -87,8 +87,8 @@ interface YSPBtraceService {
"referer: https://www.yangshipin.cn/",
)
fun kvcollect3(
@Field("BossId") BossId: String = "9141",
@Field("Pwd") Pwd: String = "1137850982",
@Field("BossId") BossId: String = "9150",
@Field("Pwd") Pwd: String = "1999332929",
@Field("prog") prog: String = "", //
@Field("playno") playno: String = "", //
@Field("guid") guid: String = "", //
@@ -102,7 +102,7 @@ interface YSPBtraceService {
@Field("firstreport") firstreport: String = "-",
@Field("sUrl") sUrl: String = "", //
@Field("sRef") sRef: String = "-",
@Field("fplayerver") fplayerver: String = "100",
@Field("fplayerver") fplayerver: String = "180",
@Field("livepid") livepid: String = "",
@Field("viewid") viewid: String = "",
@Field("seq") seq: String = "",
@@ -118,8 +118,8 @@ interface YSPBtraceService {
@Field("_dc") _dc: String = "",
@Field("live_type") live_type: String = "-",
@Field("ftime") ftime: String = "",
@Field("url") url: String = "",
@Field("rand_str") rand_str: String = "", //
@Field("signature") signature: String = "",
// @Field("url") url: String = "",
// @Field("rand_str") rand_str: String = "", //
// @Field("signature") signature: String = "",
): Call<Void>
}
@@ -0,0 +1,19 @@
package com.lizongying.mytv.models
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class MyViewModel : ViewModel() {
private var _downloadProgress = MutableLiveData<Int>()
val downloadProgress: LiveData<Int>
get() = _downloadProgress
fun setDownloadProgress(progress: Int) {
_downloadProgress.value = progress
}
init {
_downloadProgress.value = 0
}
}
@@ -1,4 +1,4 @@
package com.lizongying.mytv
package com.lizongying.mytv.models
import com.lizongying.mytv.models.ProgramType
import java.io.Serializable
@@ -15,18 +15,22 @@ data class TV(
var programType: ProgramType,
var needToken: Boolean = false,
var mustToken: Boolean = false,
) : Serializable {
var volume: Float = 0.1F,
) : Serializable {
override fun toString(): String {
return "TV{" +
"id=" + id +
", title='" + title + '\'' +
", alias='" + alias + '\'' +
", videoUrl='" + videoUrl + '\'' +
", channel='" + channel + '\'' +
", logo='" + logo + '\'' +
", pid='" + pid + '\'' +
", sid='" + sid + '\'' +
", programType='" + programType + '\'' +
", needToken='" + needToken + '\'' +
", mustToken='" + mustToken + '\'' +
'}'
}
}
@@ -1,6 +1,6 @@
package com.lizongying.mytv
package com.lizongying.mytv.models
import com.lizongying.mytv.models.ProgramType
import com.lizongying.mytv.R
object TVList {
val list: Map<String, List<TV>> by lazy {
@@ -21,7 +21,8 @@ object TVList {
"2022576801",
ProgramType.Y_PROTO,
false,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -34,7 +35,8 @@ object TVList {
"2022576701",
ProgramType.Y_PROTO,
true,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -47,7 +49,8 @@ object TVList {
"2022576501",
ProgramType.Y_PROTO,
true,
mustToken = true
mustToken = true,
0.5F
),
TV(
0,
@@ -60,7 +63,8 @@ object TVList {
"2022576601",
ProgramType.Y_PROTO,
false,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -73,7 +77,8 @@ object TVList {
"2022576401",
ProgramType.Y_PROTO,
true,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -86,7 +91,8 @@ object TVList {
"2013693901",
ProgramType.Y_PROTO,
true,
mustToken = true
mustToken = true,
0.5F
),
TV(
0,
@@ -99,7 +105,8 @@ object TVList {
"2022576201",
ProgramType.Y_PROTO,
true,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -112,7 +119,8 @@ object TVList {
"2022576101",
ProgramType.Y_PROTO,
true,
mustToken = true
mustToken = true,
0.5F
),
TV(
0,
@@ -125,7 +133,8 @@ object TVList {
"2022576001",
ProgramType.Y_PROTO,
true,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -138,7 +147,8 @@ object TVList {
"2022573001",
ProgramType.Y_PROTO,
true,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -151,7 +161,8 @@ object TVList {
"2022575901",
ProgramType.Y_PROTO,
true,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -164,7 +175,8 @@ object TVList {
"2022575801",
ProgramType.Y_PROTO,
true,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -177,7 +189,8 @@ object TVList {
"2022575701",
ProgramType.Y_PROTO,
false,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -190,7 +203,8 @@ object TVList {
"2022575601",
ProgramType.Y_PROTO,
true,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -203,7 +217,8 @@ object TVList {
"2022575501",
ProgramType.Y_PROTO,
true,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -216,7 +231,8 @@ object TVList {
"2022575401",
ProgramType.Y_PROTO,
true,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -229,7 +245,8 @@ object TVList {
"2022575301",
ProgramType.Y_PROTO,
true,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -242,7 +259,8 @@ object TVList {
"2022576301",
ProgramType.Y_PROTO,
true,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -255,7 +273,8 @@ object TVList {
"2022575201",
ProgramType.Y_PROTO,
true,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -268,7 +287,8 @@ object TVList {
"2020603421",
ProgramType.Y_JCE,
false,
mustToken = false
mustToken = false,
0.8F
),
TV(
0,
@@ -738,7 +758,8 @@ object TVList {
"2019927403",
ProgramType.Y_JCE,
true,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -751,7 +772,8 @@ object TVList {
"2022606701",
ProgramType.Y_JCE,
true,
mustToken = false
mustToken = false,
0.8F
),
TV(
0,
@@ -764,7 +786,8 @@ object TVList {
"2022823801",
ProgramType.Y_JCE,
true,
mustToken = false
mustToken = false,
0.6F
),
),
"港澳台" to listOf(
@@ -779,7 +802,8 @@ object TVList {
"",
ProgramType.F,
false,
mustToken = false
mustToken = false,
0.7F
),
TV(
0,
@@ -792,7 +816,8 @@ object TVList {
"",
ProgramType.F,
false,
mustToken = false
mustToken = false,
0.7F
),
TV(
0,
@@ -805,7 +830,8 @@ object TVList {
"",
ProgramType.F,
false,
mustToken = false
mustToken = false,
0.7F
),
),
"国际" to listOf(
@@ -820,7 +846,8 @@ object TVList {
"2022575001",
ProgramType.Y_PROTO,
false,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -833,7 +860,8 @@ object TVList {
"2022574901",
ProgramType.Y_PROTO,
false,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -846,7 +874,8 @@ object TVList {
"2022574801",
ProgramType.Y_PROTO,
false,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -859,7 +888,8 @@ object TVList {
"2022574601",
ProgramType.Y_PROTO,
false,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -872,7 +902,8 @@ object TVList {
"2022571701",
ProgramType.Y_PROTO,
false,
mustToken = false
mustToken = false,
0.5F
),
TV(
0,
@@ -885,7 +916,8 @@ object TVList {
"2022574701",
ProgramType.Y_PROTO,
false,
mustToken = false
mustToken = false,
0.5F
),
)
)
@@ -900,6 +932,7 @@ object TVList {
v.forEach { v1 ->
if (!v1.mustToken) {
v1.id = id
v1.needToken = false
id++
group.add(v1)
}
@@ -3,6 +3,7 @@ package com.lizongying.mytv.models
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.lizongying.mytv.SP
class TVListViewModel : ViewModel() {
@@ -16,10 +17,6 @@ class TVListViewModel : ViewModel() {
val itemPosition: LiveData<Int>
get() = _itemPosition
private val _itemPositionCurrent = MutableLiveData<Int>()
val itemPositionCurrent: LiveData<Int>
get() = _itemPositionCurrent
fun addTVViewModel(tvViewModel: TVViewModel) {
if (_tvListViewModel.value == null) {
_tvListViewModel.value = mutableListOf(tvViewModel)
@@ -32,23 +29,16 @@ class TVListViewModel : ViewModel() {
return _tvListViewModel.value?.get(id)
}
fun getTVViewModelCurrent(): TVViewModel? {
return _itemPositionCurrent.value?.let { _tvListViewModel.value?.get(it) }
}
fun setItemPosition(position: Int) {
_itemPosition.value = position
_itemPositionCurrent.value = position
}
fun setItemPositionCurrent(position: Int) {
_itemPositionCurrent.value = position
SP.itemPosition = position
}
fun size(): Int {
if (_tvListViewModel.value == null) {
return 0
}
return _tvListViewModel.value!!.size
}
}
@@ -1,10 +1,8 @@
package com.lizongying.mytv.models
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.lizongying.mytv.TV
import com.lizongying.mytv.api.FEPG
import com.lizongying.mytv.proto.Ysp.cn.yangshipin.omstv.common.proto.programModel.Program
import com.tencent.videolite.android.datamodel.cctvjce.TVProgram
@@ -18,6 +16,8 @@ class TVViewModel(private var tv: TV) : ViewModel() {
var retryTimes = 0
var retryMaxTimes = 8
var authYSPRetryTimes = 0
var authYSPRetryMaxTimes = 3
var tokenYSPRetryTimes = 0
var tokenYSPRetryMaxTimes = 0
var tokenFHRetryTimes = 0
@@ -41,8 +41,8 @@ class TVViewModel(private var tv: TV) : ViewModel() {
val videoIndex: LiveData<Int>
get() = _videoIndex
private val _change = MutableLiveData<Boolean>()
val change: LiveData<Boolean>
private val _change = MutableLiveData<String>()
val change: LiveData<String>
get() = _change
private val _ready = MutableLiveData<Boolean>()
@@ -56,36 +56,27 @@ class TVViewModel(private var tv: TV) : ViewModel() {
if (_videoUrl.value!!.last().contains("cctv.cn")) {
tv.videoUrl = tv.videoUrl.subList(0, tv.videoUrl.lastIndex) + listOf(url)
} else {
tv.videoUrl = tv.videoUrl + listOf(url)
tv.videoUrl += listOf(url)
}
} else {
tv.videoUrl = tv.videoUrl + listOf(url)
tv.videoUrl += listOf(url)
}
_videoUrl.value = tv.videoUrl
_videoIndex.value = tv.videoUrl.lastIndex
}
fun firstSource() {
if (_videoUrl.value!!.isNotEmpty()) {
setVideoIndex(0)
allReady()
} else {
Log.e(TAG, "no first")
}
}
fun changed() {
_change.value = true
fun changed(from: String) {
retryTimes = 0
authYSPRetryTimes = 0
tokenYSPRetryTimes = 0
tokenFHRetryTimes = 0
_change.value = from
}
fun allReady() {
_ready.value = true
}
fun setVideoIndex(videoIndex: Int) {
_videoIndex.value = videoIndex
}
init {
_videoUrl.value = tv.videoUrl
_videoIndex.value = tv.videoUrl.lastIndex
@@ -10,20 +10,23 @@ import retrofit2.Response
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class MyRequest {
class ReleaseRequest {
private var releaseService = ApiClient().releaseService
suspend fun getRelease(): ReleaseV2? {
suspend fun getRelease(): ReleaseResponse? {
return withContext(Dispatchers.IO) {
fetchRelease()
}
}
private suspend fun fetchRelease(): ReleaseV2? {
private suspend fun fetchRelease(): ReleaseResponse? {
return suspendCoroutine { continuation ->
releaseService.getRelease()
.enqueue(object : Callback<ReleaseV2> {
override fun onResponse(call: Call<ReleaseV2>, response: Response<ReleaseV2>) {
.enqueue(object : Callback<ReleaseResponse> {
override fun onResponse(
call: Call<ReleaseResponse>,
response: Response<ReleaseResponse>
) {
if (response.isSuccessful) {
continuation.resume(response.body())
} else {
@@ -31,7 +34,7 @@ class MyRequest {
}
}
override fun onFailure(call: Call<ReleaseV2>, t: Throwable) {
override fun onFailure(call: Call<ReleaseResponse>, t: Throwable) {
continuation.resume(null)
}
})
@@ -39,6 +42,6 @@ class MyRequest {
}
companion object {
private const val TAG = "MyRequest"
private const val TAG = "ReleaseRequest"
}
}
@@ -0,0 +1,7 @@
package com.lizongying.mytv.requests
data class ReleaseResponse(
val version_code: Int?,
val version_name: String?,
)
@@ -0,0 +1,10 @@
package com.lizongying.mytv.requests
import retrofit2.Call
import retrofit2.http.GET
interface ReleaseService {
@GET("JELLY_BEAN_MR1/version.json")
fun getRelease(
): Call<ReleaseResponse>
}
@@ -1,9 +1,10 @@
package com.lizongying.mytv
package com.lizongying.mytv.requests
import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.util.Log
import com.lizongying.mytv.Utils
import com.lizongying.mytv.Utils.getDateFormat
import com.lizongying.mytv.api.ApiClient
import com.lizongying.mytv.api.Auth
@@ -17,12 +18,14 @@ import com.lizongying.mytv.api.KvcollectRequest
import com.lizongying.mytv.api.KvcollectRequest2
import com.lizongying.mytv.api.LiveInfo
import com.lizongying.mytv.api.LiveInfoRequest
import com.lizongying.mytv.api.Token2
import com.lizongying.mytv.api.YSP
import com.lizongying.mytv.api.YSPApiService
import com.lizongying.mytv.api.YSPBtraceService
import com.lizongying.mytv.api.YSPJceService
import com.lizongying.mytv.api.YSPProtoService
import com.lizongying.mytv.api.YSPTokenService
import com.lizongying.mytv.models.TVList
import com.lizongying.mytv.models.TVViewModel
import com.lizongying.mytv.proto.Ysp.cn.yangshipin.oms.common.proto.pageModel
import com.lizongying.mytv.proto.Ysp.cn.yangshipin.omstv.common.proto.epgProgramModel
@@ -40,6 +43,7 @@ import kotlin.random.Random
object Request {
private const val TAG = "Request"
private var yspTokenService: YSPTokenService = ApiClient().yspTokenService
private var yspToken2Service: YSPApiService = ApiClient().yspToken2Service
private var yspApiService: YSPApiService = ApiClient().yspApiService
private var yspBtraceService: YSPBtraceService = ApiClient().yspBtraceService
private var yspBtraceService2: YSPBtraceService = ApiClient().yspBtraceService2
@@ -52,8 +56,9 @@ object Request {
private var token = ""
private var tokenFH = ""
private var needAuth = false
private var needAuth = true
private var needToken = false
private var needToken2 = true
private val handler = Handler(Looper.getMainLooper())
private lateinit var btraceRunnable: BtraceRunnable
@@ -72,6 +77,7 @@ object Request {
}
private var call: Call<LiveInfo>? = null
private var callToken: Call<Token2>? = null
private var callAuth: Call<Auth>? = null
private var callInfo: Call<Info>? = null
private var callFAuth: Call<FAuth>? = null
@@ -82,11 +88,117 @@ object Request {
private fun cancelCall() {
call?.cancel()
callAuth?.cancel()
callToken?.cancel()
callInfo?.cancel()
callFAuth?.cancel()
callPage?.cancel()
}
private fun fetchToken(tvModel: TVViewModel, cookie: String) {
cancelCall()
val title = tvModel.getTV().title
callToken = yspToken2Service.getToken("guid=${YSP.getGuid()}; $cookie", "519748109", YSP.getGuid(), "59306155", "b42702bf7309a179d102f3d51b1add2fda0bc7ada64cb801" ,"1", YSP.getTimeStr())
callToken?.enqueue(object : Callback<Token2> {
override fun onResponse(call: Call<Token2>, response: Response<Token2>) {
if (response.isSuccessful) {
val liveInfo = response.body()
if (liveInfo?.data?.token != null) {
Log.i(TAG, "token ${liveInfo.data.token}")
YSP.token2 = liveInfo.data.token
YSP.getTokenData(tvModel)
fetchAuth(tvModel, cookie)
} else {
Log.e(TAG, "$title token error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
if (tvModel.getTV().needToken) {
if (needToken && tvModel.tokenYSPRetryTimes < tvModel.tokenYSPRetryMaxTimes) {
tvModel.tokenYSPRetryTimes++
tvModel.needGetToken = true
fetchToken(tvModel)
} else {
if (!tvModel.getTV().mustToken) {
fetchToken(tvModel, cookie)
} else {
val err = "错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
} else {
fetchToken(tvModel, cookie)
}
} else {
val err = "认证结果错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
} else {
Log.e(TAG, "$title token status error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
if (tvModel.getTV().needToken) {
if (needToken && tvModel.tokenYSPRetryTimes < tvModel.tokenYSPRetryMaxTimes) {
tvModel.tokenYSPRetryTimes++
tvModel.needGetToken = true
fetchToken(tvModel)
} else {
if (!tvModel.getTV().mustToken) {
fetchToken(tvModel, cookie)
} else {
val err = "错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
} else {
fetchToken(tvModel, cookie)
}
} else {
val err = "认证状态错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
}
override fun onFailure(call: Call<Token2>, t: Throwable) {
Log.e(TAG, "$title token request error $t")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
if (tvModel.getTV().needToken) {
if (needToken && tvModel.tokenYSPRetryTimes < tvModel.tokenYSPRetryMaxTimes) {
tvModel.tokenYSPRetryTimes++
tvModel.needGetToken = true
fetchToken(tvModel)
} else {
if (!tvModel.getTV().mustToken) {
fetchToken(tvModel, cookie)
} else {
val err = "错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
} else {
fetchToken(tvModel, cookie)
}
} else {
val err = "认证请求错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
})
}
private fun fetchAuth(tvModel: TVViewModel, cookie: String) {
cancelCall()
@@ -101,11 +213,11 @@ object Request {
val liveInfo = response.body()
if (liveInfo?.data?.token != null) {
Log.i(TAG, "token ${liveInfo.data.token}")
Log.i(TAG, "auth ${liveInfo.data.token}")
YSP.token = liveInfo.data.token
fetchVideo(tvModel, cookie)
} else {
Log.e(TAG, "$title token error")
Log.e(TAG, "$title auth error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
if (tvModel.getTV().needToken) {
@@ -116,11 +228,19 @@ object Request {
} else {
if (!tvModel.getTV().mustToken) {
fetchAuth(tvModel, cookie)
} else {
val err = "错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
} else {
fetchAuth(tvModel, cookie)
}
} else {
val err = "认证结果错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
} else {
@@ -135,11 +255,19 @@ object Request {
} else {
if (!tvModel.getTV().mustToken) {
fetchAuth(tvModel, cookie)
} else {
val err = "错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
} else {
fetchAuth(tvModel, cookie)
}
} else {
val err = "认证状态错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
}
@@ -156,11 +284,19 @@ object Request {
} else {
if (!tvModel.getTV().mustToken) {
fetchAuth(tvModel, cookie)
} else {
val err = "错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
} else {
fetchAuth(tvModel, cookie)
}
} else {
val err = "认证请求错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
})
@@ -169,7 +305,7 @@ object Request {
private fun fetchVideo(tvModel: TVViewModel, cookie: String) {
cancelCall()
if (::btraceRunnable.isInitialized) {
if (Request::btraceRunnable.isInitialized) {
handler.removeCallbacks(btraceRunnable)
}
@@ -180,8 +316,14 @@ object Request {
val request = LiveInfoRequest(data)
call = request.let {
yspApiService.getLiveInfo(
"guid=${YSP.getGuid()}; $cookie",
"guid=${YSP.getGuid()}; gr_user_id=1; $cookie; newLogin=1; nseqId=${YSP.nseqId}; nrequest-id=${YSP.nrequest_id}",
YSP.token,
YSP.yspsdkinput,
YSP.openapi_signature,
YSP.yspticket,
"${YSP.nseqId}",
YSP.nrequest_id,
"519748109",
it
)
}
@@ -192,21 +334,8 @@ object Request {
val liveInfo = response.body()
if (liveInfo?.data?.playurl != null) {
val chanll = liveInfo.data.chanll
val decodedBytes = Base64.decode(
chanll.substring(9, chanll.length - 3),
Base64.DEFAULT
)
val decodedString = String(decodedBytes)
val matchResult = regex.find(decodedString)
if (matchResult != null) {
val (key, iv) = matchResult.destructured
val keyBytes = Base64.decode(key, Base64.DEFAULT)
val ivBytes = Base64.decode(iv, Base64.DEFAULT)
val url = liveInfo.data.playurl + "&revoi=" + encryptTripleDES(
keyBytes + byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0),
ivBytes
).uppercase() + liveInfo.data.extended_param
if (liveInfo.data.extended_param != null) {
val url = liveInfo.data.playurl + "&revoi=" + liveInfo.data.extended_param
Log.d(TAG, "$title url $url")
tvModel.addVideoUrl(url)
tvModel.allReady()
@@ -221,24 +350,42 @@ object Request {
if (needToken && tvModel.tokenYSPRetryTimes < tvModel.tokenYSPRetryMaxTimes) {
tvModel.tokenYSPRetryTimes++
tvModel.needGetToken = true
fetchVideo(tvModel)
// fetchAuth(tvModel)
if (needAuth) {
fetchAuth(tvModel)
} else {
fetchVideo(tvModel)
}
} else {
if (!tvModel.getTV().mustToken) {
fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
if (needAuth) {
fetchAuth(tvModel, cookie)
} else {
fetchVideo(tvModel, cookie)
}
} else {
val err = "错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
} else {
fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
if (needAuth) {
fetchAuth(tvModel, cookie)
} else {
fetchVideo(tvModel, cookie)
}
}
} else {
val err = "结果错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
} else {
if (liveInfo?.data?.errinfo != null && liveInfo.data.errinfo == "应版权方要求,暂停提供直播信号,请点击观看其他精彩节目") {
Log.e(TAG, "$title error ${liveInfo.data.errinfo}")
tvModel.setErrInfo(liveInfo.data.errinfo)
val err = "版权错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
} else {
Log.e(TAG, "$title url error $request $liveInfo")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
@@ -247,18 +394,35 @@ object Request {
if (needToken && tvModel.tokenYSPRetryTimes < tvModel.tokenYSPRetryMaxTimes) {
tvModel.tokenYSPRetryTimes++
tvModel.needGetToken = true
fetchVideo(tvModel)
// fetchAuth(tvModel)
if (needAuth) {
fetchAuth(tvModel)
} else {
fetchVideo(tvModel)
}
} else {
if (!tvModel.getTV().mustToken) {
fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
if (needAuth) {
fetchAuth(tvModel, cookie)
} else {
fetchVideo(tvModel, cookie)
}
} else {
val err = "错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
} else {
fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
if (needAuth) {
fetchAuth(tvModel, cookie)
} else {
fetchVideo(tvModel, cookie)
}
}
} else {
val err = "其他错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
}
@@ -270,18 +434,35 @@ object Request {
if (needToken && tvModel.tokenYSPRetryTimes < tvModel.tokenYSPRetryMaxTimes) {
tvModel.tokenYSPRetryTimes++
tvModel.needGetToken = true
fetchVideo(tvModel)
// fetchAuth(tvModel)
if (needAuth) {
fetchAuth(tvModel, cookie)
} else {
fetchVideo(tvModel, cookie)
}
} else {
if (!tvModel.getTV().mustToken) {
fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
if (needAuth) {
fetchAuth(tvModel, cookie)
} else {
fetchVideo(tvModel, cookie)
}
} else {
val err = "错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
} else {
fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
if (needAuth) {
fetchAuth(tvModel, cookie)
} else {
fetchVideo(tvModel, cookie)
}
}
} else {
val err = "状态错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
}
@@ -298,16 +479,82 @@ object Request {
} else {
if (!tvModel.getTV().mustToken) {
fetchVideo(tvModel, cookie)
} else {
// TODO
}
}
} else {
fetchVideo(tvModel, cookie)
}
} else {
val err = "请求错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
})
}
private fun fetchToken(tvModel: TVViewModel) {
cancelCall()
if (tvModel.needGetToken) {
callInfo = yspTokenService.getInfo("")
callInfo?.enqueue(object : Callback<Info> {
override fun onResponse(call: Call<Info>, response: Response<Info>) {
if (response.isSuccessful && response.body()?.data?.token != null) {
token = response.body()?.data?.token!!
Log.i(TAG, "info success $token")
tvModel.needGetToken = false
tvModel.tokenYSPRetryTimes = 0
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=124; appid=1400421205; yspappid=519748109;yspopenid=$openid; vusession=$token"
fetchAuth(tvModel, cookie)
} else if (response.code() == 304) {
tvModel.needGetToken = false
tvModel.tokenYSPRetryTimes = 0
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=124; appid=1400421205; yspappid=519748109; yspopenid=$openid; vusession=$token"
fetchVideo(tvModel, cookie)
} else {
Log.e(TAG, "info status error")
if (tvModel.tokenYSPRetryTimes < tvModel.tokenYSPRetryMaxTimes) {
tvModel.tokenYSPRetryTimes++
fetchToken(tvModel)
} else {
if (!tvModel.getTV().mustToken) {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=124; appid=1400421205; yspappid=519748109"
fetchToken(tvModel, cookie)
} else {
// TODO
}
}
}
}
override fun onFailure(call: Call<Info>, t: Throwable) {
Log.e(TAG, "info request error $t")
if (tvModel.tokenYSPRetryTimes < tvModel.tokenYSPRetryMaxTimes) {
tvModel.tokenYSPRetryTimes++
fetchAuth(tvModel)
} else {
if (!tvModel.getTV().mustToken) {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=124; appid=1400421205; yspappid=519748109"
fetchToken(tvModel, cookie)
} else {
// TODO
}
}
}
})
} else {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=124; appid=1400421205; yspappid=519748109;yspopenid=$openid; vusession=$token"
fetchToken(tvModel, cookie)
}
}
private fun fetchAuth(tvModel: TVViewModel) {
cancelCall()
if (tvModel.needGetToken) {
@@ -338,6 +585,8 @@ object Request {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109"
fetchAuth(tvModel, cookie)
} else {
// TODO
}
}
}
@@ -353,13 +602,15 @@ object Request {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109"
fetchAuth(tvModel, cookie)
} else {
// TODO
}
}
}
})
} else {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109;yspopenid=$openid; vusession=$token"
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=124; yspopenid=$openid; vusession=$token"
fetchAuth(tvModel, cookie)
}
}
@@ -395,6 +646,8 @@ object Request {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109"
fetchVideo(tvModel, cookie)
} else {
// TODO
}
}
}
@@ -410,6 +663,8 @@ object Request {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109"
fetchVideo(tvModel, cookie)
} else {
// TODO
}
}
}
@@ -445,6 +700,10 @@ object Request {
if (tvModel.tokenFHRetryTimes < tvModel.tokenFHRetryMaxTimes) {
tvModel.tokenFHRetryTimes++
fetchFAuth(tvModel)
} else {
val err = "状态错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
}
@@ -454,6 +713,10 @@ object Request {
if (tvModel.tokenFHRetryTimes < tvModel.tokenFHRetryMaxTimes) {
tvModel.tokenFHRetryTimes++
fetchFAuth(tvModel)
} else {
val err = "请求错误"
Log.e(TAG, "$title $err")
tvModel.setErrInfo(err)
}
}
})
@@ -469,19 +732,42 @@ object Request {
if (token == "") {
tvModel.needGetToken = true
}
if (needAuth) {
fetchAuth(tvModel)
if (needToken2) {
if (needAuth) {
fetchToken(tvModel)
} else {
fetchAuth(tvModel)
}
} else {
fetchVideo(tvModel)
if (needAuth) {
fetchAuth(tvModel)
} else {
fetchVideo(tvModel)
}
}
} else {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; updateProtocol=1; deviceModel=120"
if (needAuth) {
fetchAuth(tvModel, cookie)
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=124"
if (needToken2) {
if (needAuth) {
fetchToken(tvModel, cookie)
} else {
fetchAuth(tvModel, cookie)
}
} else {
fetchVideo(tvModel, cookie)
if (needAuth) {
fetchAuth(tvModel, cookie)
} else {
fetchVideo(tvModel, cookie)
}
}
//
// if (needAuth) {
// fetchAuth(tvModel, cookie)
// } else {
// fetchVideo(tvModel, cookie)
// }
}
}
@@ -600,7 +886,7 @@ object Request {
seq = tvModel.seq,
durl = url,
url = url,
_dc = randomNumber,
// _dc = "",
)
val e =
@@ -632,7 +918,7 @@ object Request {
val guid = YSP.getGuid()
val pid = tvModel.getTV().pid
val sid = tvModel.getTV().sid
val randomNumber = Random.nextDouble()
// val randomNumber = Random.nextDouble()
val url = tvModel.getTV().videoUrl.first()
val r = KvcollectRequest2(
@@ -647,12 +933,12 @@ object Request {
seq = tvModel.seq,
durl = url,
url = url,
_dc = randomNumber,
_dc = "",
)
val e =
"BossId=${r.BossId}&Pwd=${r.Pwd}&_dc=${r._dc}&cdn=${r.cdn}&cmd=${r.cmd}&defn=${r.defn}&downspeed=${r.downspeed}&durl=${r.durl}&errcode=${r.errcode}&fact1=${r.fact1}&firstreport=${r.firstreport}&fplayerver=${r.fplayerver}&ftime=${r.ftime}&geturltime=${r.geturltime}&guid=${r.guid}&hc_openid=${r.hc_openid}&hh_ua=${r.hh_ua}&live_type=${r.live_type}&livepid=${r.livepid}&login_type=${r.login_type}&open_id=${r.open_id}&openid=${r.openid}&platform=${r.platform}&playno=${r.playno}&prd=${r.prd}&prog=${r.prog}&rand_str=${r.rand_str}&sRef=${r.sRef}&sUrl=${r.sUrl}&sdtfrom=${r.sdtfrom}&seq=${r.seq}&url=${r.url}&viewid=${r.viewid}"
r.signature = YSP.getAuthSignature(e)
// val e =
// "BossId=${r.BossId}&Pwd=${r.Pwd}&_dc=${r._dc}&cdn=${r.cdn}&cmd=${r.cmd}&defn=${r.defn}&downspeed=${r.downspeed}&durl=${r.durl}&errcode=${r.errcode}&fact1=${r.fact1}&firstreport=${r.firstreport}&fplayerver=${r.fplayerver}&ftime=${r.ftime}&geturltime=${r.geturltime}&guid=${r.guid}&hc_openid=${r.hc_openid}&hh_ua=${r.hh_ua}&live_type=${r.live_type}&livepid=${r.livepid}&login_type=${r.login_type}&open_id=${r.open_id}&openid=${r.openid}&platform=${r.platform}&playno=${r.playno}&prd=${r.prd}&prog=${r.prog}&rand_str=${r.rand_str}&sRef=${r.sRef}&sUrl=${r.sUrl}&sdtfrom=${r.sdtfrom}&seq=${r.seq}&url=${r.url}&viewid=${r.viewid}"
// r.signature = YSP.getAuthSignature(e)
callBtracePage = yspBtraceService3.kvcollect3(
guid = r.guid,
@@ -661,18 +947,18 @@ object Request {
livepid = r.livepid,
sUrl = r.sUrl,
playno = r.playno,
rand_str = r.rand_str,
// rand_str = r.rand_str,
ftime = r.ftime,
seq = "${r.seq}",
durl = r.durl,
url = r.url,
// url = r.url,
_dc = "${r._dc}",
signature = r.signature
// signature = r.signature
)
callBtracePage?.enqueue(object : Callback<Void> {
override fun onResponse(call: Call<Void>, response: Response<Void>) {
if (response.isSuccessful) {
Log.d(TAG, "$title kvcollect success")
Log.i(TAG, "$title kvcollect success")
} else {
Log.e(TAG, "$title kvcollect status error")
}
@@ -815,6 +1101,6 @@ object Request {
}
fun setRequestListener(listener: RequestListener) {
this.listener = listener
Request.listener = listener
}
}
@@ -1,4 +0,0 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FF263238" />
<corners android:radius="4dp" /> <!-- Adjust the radius as needed -->
</shape>
@@ -0,0 +1,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/blur" />
<corners
android:bottomLeftRadius="4dp"
android:bottomRightRadius="4dp" />
</shape>
@@ -0,0 +1,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/blur" />
<corners
android:topLeftRadius="4dp"
android:bottomLeftRadius="4dp" />
</shape>
@@ -0,0 +1,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/blur" />
<corners
android:topRightRadius="4dp"
android:bottomRightRadius="4dp" />
</shape>
@@ -0,0 +1,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/focus" />
<corners
android:bottomLeftRadius="4dp"
android:bottomRightRadius="4dp" />
</shape>
@@ -0,0 +1,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/white" />
<corners
android:topLeftRadius="4dp"
android:bottomLeftRadius="4dp" />
</shape>
@@ -0,0 +1,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/white" />
<corners
android:topRightRadius="4dp"
android:bottomRightRadius="4dp" />
</shape>
@@ -1,4 +1,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/white" />
<corners android:radius="4dp" /> <!-- Adjust the radius as needed -->
<corners
android:topLeftRadius="4dp"
android:topRightRadius="4dp" />
</shape>
+23
View File
@@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportWidth="50"
android:viewportHeight="50"
android:width="62.5dp"
android:height="62.5dp">
<group
android:scaleX="0.1"
android:scaleY="-0.1"
android:translateY="50">
<path
android:pathData="M205 377c-27 -27 -40 -33 -65 -29 -30 4 -70 -19 -70 -40 0 -5 -16 -24 -35 -42 -38 -36 -44 -66 -23 -111 22 -49 51 -55 248 -55l182 0 29 29c46 46 37 106 -20 136 -20 10 -31 25 -35 49 -4 22 -20 46 -42 65 -29 26 -43 31 -85 31 -44 0 -55 -4 -84 -33zm138 2c29 -13 57 -58 57 -93 0 -17 5 -26 15 -26 24 0 65 -46 65 -71 0 -13 -10 -33 -22 -46 -21 -22 -26 -23 -205 -23 -182 0 -184 0 -208 25 -33 32 -32 70 3 105 16 15 36 40 45 54 18 27 54 35 74 15 8 -8 16 -2 29 19 9 17 27 35 38 41 28 13 80 13 109 0z"
android:fillColor="#FFEEEEEE" />
<path
android:pathData="M174 269c-10 -17 13 -36 27 -22 12 12 4 33 -11 33 -5 0 -12 -5 -16 -11z"
android:fillColor="#FFEEEEEE" />
<path
android:pathData="M294 269c-10 -17 13 -36 27 -22 12 12 4 33 -11 33 -5 0 -12 -5 -16 -11z"
android:fillColor="#FFEEEEEE" />
<path
android:pathData="M194 196c-32 -14 -50 -33 -41 -42 3 -3 20 4 38 15 43 26 75 26 118 0 34 -20 49 -20 36 1 -10 16 -69 40 -97 40 -13 -1 -37 -7 -54 -14z"
android:fillColor="#FFEEEEEE" />
</group>
</vector>
+60
View File
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/card"
android:layout_width="150dp"
android:layout_height="100dp"
android:orientation="vertical"
android:layout_gravity="center_vertical"
>
<ImageView
android:id="@+id/icon"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@drawable/rounded_white_top"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:padding="5dp" />
<LinearLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="60dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/icon"
android:background="@drawable/rounded_dark_bottom"
android:orientation="vertical"
android:padding="7dp">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:gravity="start"
android:text="Title"
android:maxLines="1"
android:ellipsize="end"
android:textColor="#FFEEEEEE"
android:textSize="14sp" />
<TextView
android:id="@+id/desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
android:layout_marginTop="8dp"
android:gravity="start"
android:maxLines="1"
android:ellipsize="end"
android:text="Description"
android:textColor="#B3EEEEEE"
android:textSize="12sp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
+42 -17
View File
@@ -1,19 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/channel_fragment"
android:layout_width="90dp"
android:layout_height="60dp"
android:layout_gravity="end|top"
android:layout_marginTop="20dp"
android:layout_marginEnd="170dp">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
>
<FrameLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<TextView
android:id="@+id/channel_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="0dp"
android:textColor="#FFEEEEEE"
android:textStyle="bold"
android:textSize="40sp" />
</FrameLayout>
<RelativeLayout
android:id="@+id/channel"
android:layout_width="90dp"
android:layout_height="60dp"
android:layout_gravity="end|top"
android:layout_marginTop="15dp"
android:layout_marginEnd="170dp"
android:gravity="end|bottom" >
<TextView
android:id="@+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:textColor="@color/white"
android:textStyle="bold"
android:textSize="40sp"
tools:text="99"
android:layout_alignBaseline="@+id/time"/>
<TextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:textColor="@color/white"
android:textStyle="bold"
android:textSize="32sp"
tools:text="12:34" />
</RelativeLayout>
</FrameLayout>
</LinearLayout>
+24
View File
@@ -0,0 +1,24 @@
<!-- res/layout/fragment_progress_dialog.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:padding="20dp">
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/start_update"
tools:text="开始更新">
</Button>
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="false"
android:max="100"
android:progress="0"
android:progressDrawable="@android:drawable/progress_horizontal" />
</LinearLayout>
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/error"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black" >
<LinearLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical" >
<ImageView
android:id="@+id/logo"
android:src="@drawable/sad_cloud"
android:layout_width="50dp"
android:layout_height="50dp" />
<TextView
android:id="@+id/msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="#FFEEEEEE"
android:text="Message"
android:textSize="20sp" />
</LinearLayout>
</FrameLayout>
+55 -48
View File
@@ -1,53 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/info_fragment"
android:layout_width="320dp"
android:layout_height="80dp"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="20dp"
android:background="@drawable/rounded_background">
<ImageView
android:id="@+id/info_logo"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:padding="10dp" />
<LinearLayout
android:layout_width="216dp"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
>
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="100dp"
android:background="#FF263238"
android:gravity="start|center_vertical"
android:orientation="vertical"
android:padding="7dp">
>
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="0dp"
android:gravity="start"
android:textColor="#FFEEEEEE"
android:textSize="22sp" />
<LinearLayout
android:id="@+id/info"
android:layout_width="400dp"
android:layout_height="80dp"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="20dp"
android:orientation="horizontal">
<TextView
android:id="@+id/info_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="8dp"
android:gravity="start"
android:singleLine="true"
android:textColor="#B3EEEEEE"
android:textSize="16sp" />
</LinearLayout>
<ImageView
android:id="@+id/logo"
android:layout_width="100dp"
android:layout_height="match_parent"
android:background="@drawable/rounded_white_left"
android:padding="10dp" />
<View
android:layout_width="8dp"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="@drawable/rounded_background2" />
</FrameLayout>
<LinearLayout
android:id="@+id/main"
android:layout_width="300dp"
android:layout_height="match_parent"
android:layout_marginStart="0dp"
android:background="@drawable/rounded_dark_right"
android:gravity="start|center_vertical"
android:orientation="vertical"
android:padding="7dp">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:gravity="start"
android:textColor="#FFEEEEEE"
android:textSize="20sp" />
<TextView
android:id="@+id/desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="8dp"
android:gravity="start"
android:singleLine="true"
android:textColor="#B3EEEEEE"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
</LinearLayout>
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
>
<LinearLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center" >
<ProgressBar
android:id="@+id/bar"
android:layout_width="50dp"
android:layout_height="50dp"
android:indeterminateTint="@color/white"/>
</LinearLayout>
</FrameLayout>
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:clickable="true"
android:focusable="false"
android:gravity="center"
>
<FrameLayout
android:id="@+id/menu"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<ScrollView
android:id="@+id/scroll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
<LinearLayout
android:id="@+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</ScrollView>
</FrameLayout>
</LinearLayout>
+2 -10
View File
@@ -1,25 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/player_fragment"
android:id="@+id/player"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:keepScreenOn="true">
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
app:resize_mode="fill"
app:use_controller="false" />
<androidx.media3.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
app:shutter_background_color="@android:color/transparent"
app:resize_mode="fill"
app:use_controller="false" />
</FrameLayout>
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@android:color/transparent"
android:clickable="true"
android:focusable="false"
>
<TextView
android:id="@+id/header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="0dp"
android:layout_marginStart="20dp"
android:layout_marginBottom="5dp"
android:gravity="start"
android:textColor="#FFEEEEEE"
android:textSize="22sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/items"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
+53 -17
View File
@@ -1,22 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dialog"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/setting"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
<LinearLayout
<ScrollView
android:id="@+id/container"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:layout_height="match_parent"
android:fillViewport="true"
>
<LinearLayout
android:id="@+id/content"
android:layout_width="300dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="20dp"
android:paddingLeft="7dp"
android:paddingRight="7dp"
android:background="#FF263238"
android:paddingBottom="20dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:background="@drawable/rounded_dark_left"
>
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
@@ -27,9 +33,10 @@
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/"
android:textSize="14sp"
/>
<LinearLayout
android:layout_width="300dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
@@ -37,7 +44,7 @@
android:id="@+id/check_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:textSize="14sp"
android:text="@string/check_version"
/>
<ProgressBar
@@ -48,45 +55,74 @@
<TextView
android:id="@+id/version_name"
android:text=""
android:textSize="14sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<!-- <androidx.core.widget.ContentLoadingProgressBar-->
<!-- android:id="@+id/progressBar"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:indeterminate="false"-->
<!-- android:max="100"-->
<!-- android:progress="0"-->
<!-- android:visibility="gone"-->
<!-- android:progressDrawable="@android:drawable/progress_horizontal" />-->
<Button
android:id="@+id/clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:text="@string/clear"
/>
<Button
android:id="@+id/exit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:textSize="14sp"
android:text="@string/exit"
/>
<Switch
android:id="@+id/switch_channel_reversal"
android:text="@string/title_channel_reversal"
android:textSize="14sp"
android:layout_marginTop="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Switch
android:id="@+id/switch_channel_num"
android:text="@string/title_channel_num"
android:textSize="14sp"
android:layout_marginTop="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Switch
android:id="@+id/switch_time"
android:text="@string/title_time"
android:textSize="14sp"
android:layout_marginTop="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Switch
android:id="@+id/switch_boot_startup"
android:text="@string/title_boot_startup"
android:textSize="14sp"
android:layout_marginTop="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Switch
android:id="@+id/switch_grid"
android:text="@string/title_grid"
android:textSize="14sp"
android:layout_marginTop="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<ImageView
android:layout_width="300dp"
android:layout_height="wrap_content"
android:src="@drawable/appreciate"
android:background="@color/white" />
</LinearLayout>
</FrameLayout>
</ScrollView>
<ImageView
android:id="@+id/appreciate"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:src="@drawable/appreciate"
android:background="@drawable/rounded_white_right" />
</LinearLayout>
+43 -17
View File
@@ -1,19 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/time"
android:layout_width="100dp"
android:layout_height="40dp"
android:layout_gravity="end|top"
android:layout_marginTop="25dp"
android:layout_marginEnd="50dp">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
>
<FrameLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<TextView
android:id="@+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="0dp"
android:textColor="#FFEEEEEE"
android:textStyle="bold"
android:textSize="32sp" />
</FrameLayout>
<RelativeLayout
android:id="@+id/time"
android:layout_width="120dp"
android:layout_height="60dp"
android:layout_gravity="end|top"
android:layout_marginTop="15dp"
android:layout_marginEnd="50dp"
android:gravity="end|bottom" >
<TextView
android:id="@+id/channel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:textColor="@color/white"
android:textStyle="bold"
android:textSize="40sp"
tools:text="99"
android:layout_alignBaseline="@+id/content"/>
<TextView
android:id="@+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:textColor="@color/white"
android:textStyle="bold"
android:textSize="32sp"
tools:text="12:34" />
</RelativeLayout>
</FrameLayout>
</LinearLayout>
+2 -2
View File
@@ -1,7 +1,7 @@
<resources>
<color name="focus">#0096a6</color>
<color name="fastlane_background">#30000000</color>
<color name="black">#000</color>
<color name="white">#FFF</color>
<color name="focus">#0096a6</color>
<color name="blur">#223239</color>
<color name="ic_launcher_background">#223239</color>
</resources>
@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="server_url">http://localhost:8080</string>
</resources>
+4 -2
View File
@@ -4,7 +4,9 @@
<string name="title_channel_num">换台时显示频道号</string>
<string name="check_version">检查更新</string>
<string name="title_boot_startup">开机自启</string>
<string name="dismiss_error">好的</string>
<string name="title_grid">频道列表采用网格样式</string>
<string name="title_time">显示时间</string>
<string name="exit">退出</string>
<string name="clear">恢复默认</string>
<string name="exit">退出应用</string>
<string name="start_update">开始更新</string>
</resources>
-7
View File
@@ -1,10 +1,3 @@
<resources>
<style name="CustomImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle">
<item name="lbImageCardViewType">Title|Content</item>
</style>
<style name="CustomImageCardTheme" parent="Theme.Leanback">
<item name="imageCardViewStyle">@style/CustomImageCardViewStyle</item>
</style>
</resources>
+1 -3
View File
@@ -1,8 +1,6 @@
<resources>
<style name="Theme.MyTV" parent="@style/Theme.Leanback">
<item name="browsePaddingStart">11dp</item>
<item name="browsePaddingEnd">11dp</item>
<item name="browseRowsMarginTop">54dp</item>
<item name="android:fitsSystemWindows">false</item>
</style>
</resources>
+3
View File
@@ -0,0 +1,3 @@
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external_files" path="." />
</paths>
+40
View File
@@ -0,0 +1,40 @@
[versions]
media3 = "1.2.1" # minSdk17
gson = "2.10.1" # 2.10.1 and older: API level 19
failureaccess = "1.0.2"
guava = "33.2.1-android"
retrofit = "2.6.4" # 21:2.9.0 17:2.6.4
protobuf = "3.25.1"
glide = "4.11.0"
work = "2.9.0"
core_ktx = "1.11.0-beta02"
multidex = "2.0.1"
leanback = "1.2.0-alpha02"
lifecycle = "2.6.2"
constraintlayout = "2.1.4"
serialization = "1.6.2"
coroutines = "1.8.0-RC"
javax_annotation = "1.3.2"
[libraries]
media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3" }
media3-common = { module = "androidx.media3:media3-common", version.ref = "media3" }
media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
media3-exoplayer-hls = { module = "androidx.media3:media3-exoplayer-hls", version.ref = "media3" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
failureaccess = { module = "com.google.guava:failureaccess", version.ref = "failureaccess" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
converter-protobuf = { module = "com.squareup.retrofit2:converter-protobuf", version.ref = "retrofit" }
protobuf-kotlin = { module = "com.google.protobuf:protobuf-kotlin", version.ref = "protobuf" }
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "work" }
core-ktx = { module = "androidx.core:core-ktx", version.ref = "core_ktx" }
multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" }
leanback = { module = "androidx.leanback:leanback", version.ref = "leanback" }
lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
javax-annotation-api = { module = "javax.annotation:javax.annotation-api", version.ref = "javax_annotation" }
+1 -1
View File
@@ -1,6 +1,6 @@
#Fri Dec 01 13:53:24 HKT 2023
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-rc-1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
Regular → Executable
View File
+1 -1
View File
@@ -1 +1 @@
{"version_code": 33619968, "version_name": "v2.1.0"}
{"version_code": 33622016, "version_name": "v2.1.8"}