Compare commits

...
3 Commits
Author SHA1 Message Date
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
26 changed files with 725 additions and 97 deletions
+11
View File
@@ -14,6 +14,16 @@
## 更新日志
### v1.5.8(通用版)
* 修复央视6画质差的问题
* 增加兵团卫视
* 播放失败重试
### v1.5.6(通用版)
* 解决部分设备系统时间不对导致播放失败的问题
### v1.5.2(通用版)
* 修复APP恢复后频道号、频道列表不自动消失的问题
@@ -109,6 +119,7 @@ adb install my-tv.apk
* 获取系统时间
* 选中的图标比例能否相差更大
* 自动重连
* 节目增加预告
## 赞赏
Binary file not shown.
Binary file not shown.
@@ -80,6 +80,11 @@ class CardAdapter(private val owner: LifecycleOwner, private var tvListViewModel
.centerInside()
.into(cardView.mainImageView)
"兵团卫视" -> Glide.with(viewHolder.view.context)
.load(R.drawable.bingtuan)
.centerInside()
.into(cardView.mainImageView)
else -> Glide.with(viewHolder.view.context)
.load(tvViewModel.logo.value)
.centerInside()
@@ -47,6 +47,11 @@ class CardPresenter(
.centerInside()
.into(cardView.mainImageView)
"兵团卫视" -> Glide.with(viewHolder.view.context)
.load(R.drawable.bingtuan)
.centerInside()
.into(cardView.mainImageView)
else -> Glide.with(viewHolder.view.context)
.load(tvViewModel.logo.value)
.centerInside()
@@ -0,0 +1,36 @@
package com.lizongying.mytv
import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
class ConfirmationDialogFragment(private val listener: ConfirmationDialogListener) :
DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
builder.setTitle("确认")
.setMessage("确认更新吗?")
.setPositiveButton(
"确定"
) { _, _ ->
listener.onConfirm()
}
.setNegativeButton(
"取消"
) { _, _ ->
listener.onCancel()
}
// 创建并返回 AlertDialog 对象
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
interface ConfirmationDialogListener {
fun onConfirm()
fun onCancel()
}
}
@@ -9,6 +9,8 @@ class Encryptor {
external fun hash(data: ByteArray): ByteArray?
external fun hash2(data: ByteArray): ByteArray?
companion object {
init {
System.loadLibrary("native")
@@ -43,6 +43,10 @@ class InfoFragment : Fragment() {
.load(R.drawable.xinjiang)
.into(binding.infoLogo)
"兵团卫视" -> Glide.with(this)
.load(R.drawable.bingtuan)
.into(binding.infoLogo)
else -> Glide.with(this)
.load(tvViewModel.logo.value)
.into(binding.infoLogo)
@@ -18,16 +18,22 @@ import android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
import android.view.WindowManager
import android.widget.Toast
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.lizongying.mytv.models.TVViewModel
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import java.security.MessageDigest
class MainActivity : FragmentActivity() {
var playerFragment = PlayerFragment()
private val mainFragment = MainFragment()
private val infoFragment = InfoFragment()
private val channelFragment = ChannelFragment()
private var ready = 0
private var playerFragment = PlayerFragment()
private var mainFragment = MainFragment()
private var infoFragment = InfoFragment()
private var channelFragment = ChannelFragment()
private lateinit var settingFragment: SettingFragment
private var doubleBackToExitPressedOnce = false
@@ -44,6 +50,16 @@ class MainActivity : FragmentActivity() {
private var versionName = ""
init {
lifecycleScope.launch(Dispatchers.IO) {
val utilsJob = async(start = CoroutineStart.LAZY) { Utils.init() }
utilsJob.start()
utilsJob.await()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
Log.i(TAG, "onCreate")
super.onCreate(savedInstanceState)
@@ -68,8 +84,14 @@ class MainActivity : FragmentActivity() {
channelReversal = sharedPref.getBoolean(CHANNEL_REVERSAL, channelReversal)
channelNum = sharedPref.getBoolean(CHANNEL_NUM, channelNum)
versionName = getPackageInfo().versionName
settingFragment = SettingFragment(versionName, channelReversal, channelNum)
val packageInfo = getPackageInfo()
versionName = packageInfo.versionName
val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode
} else {
packageInfo.versionCode.toLong()
}
settingFragment = SettingFragment(versionName, versionCode, channelReversal, channelNum)
}
fun showInfoFragment(tvViewModel: TVViewModel) {
@@ -156,7 +178,11 @@ class MainActivity : FragmentActivity() {
}
fun fragmentReady() {
mainFragment.fragmentReady()
ready++
Log.i(TAG, "ready $ready")
if (ready == 4) {
mainFragment.fragmentReady()
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
@@ -496,7 +522,7 @@ class MainActivity : FragmentActivity() {
override fun onResume() {
Log.i(TAG, "onResume")
super.onResume()
if (!mainFragment.isHidden){
if (!mainFragment.isHidden) {
handler.postDelayed(hideMain, delayHideMain)
}
}
@@ -42,8 +42,6 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
private val handler = Handler(Looper.getMainLooper())
private lateinit var mUpdateProgramRunnable: UpdateProgramRunnable
private var ready = 0
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@@ -149,7 +147,7 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
}
}
fragmentReady()
(activity as MainActivity).fragmentReady()
}
}
@@ -180,6 +178,8 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
fun setPosition() {
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
Log.i(TAG, "tvViewModel $tvViewModel")
Log.i(TAG, "rowList ${rowList.size}")
rowList[tvViewModel!!.getRowPosition()].post {
((rowList[tvViewModel.getRowPosition()] as RecyclerView).layoutManager as LinearLayoutManager).findViewByPosition(
tvViewModel.getItemPosition()
@@ -212,12 +212,8 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
}
fun fragmentReady() {
ready++
Log.i(TAG, "ready $ready")
if (ready == 4) {
// request.fetchPage()
tvListViewModel.getTVViewModel(itemPosition)?.changed()
}
tvListViewModel.getTVViewModel(itemPosition)?.changed()
}
fun play(itemPosition: Int) {
@@ -299,7 +295,9 @@ class MainFragment : Fragment(), CardAdapter.ItemListener {
override fun onDestroy() {
Log.i(TAG, "onDestroy")
super.onDestroy()
handler.removeCallbacks(mUpdateProgramRunnable)
if (::mUpdateProgramRunnable.isInitialized) {
handler.removeCallbacks(mUpdateProgramRunnable)
}
}
override fun onDestroyView() {
@@ -32,7 +32,6 @@ class PlayerFragment : Fragment() {
): View {
_binding = PlayerBinding.inflate(inflater, container, false)
playerView = _binding!!.playerView
(activity as MainActivity).playerFragment = this
playerView?.viewTreeObserver?.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
@@ -63,6 +62,7 @@ class PlayerFragment : Fragment() {
super.onPlayerError(error)
Log.e(TAG, "PlaybackException $error")
tvViewModel?.changed()
}
})
}
@@ -86,6 +86,7 @@ class PlayerFragment : Fragment() {
if (playerView != null && playerView!!.player?.isPlaying == false) {
Log.i(TAG, "replay")
playerView!!.player?.prepare()
playerView!!.player?.play()
} else {
Log.i(TAG, "playing")
}
+137 -57
View File
@@ -7,6 +7,8 @@ import android.util.Base64
import android.util.Log
import com.lizongying.mytv.Utils.getDateFormat
import com.lizongying.mytv.api.ApiClient
import com.lizongying.mytv.api.Auth
import com.lizongying.mytv.api.AuthRequest
import com.lizongying.mytv.api.Info
import com.lizongying.mytv.api.LiveInfo
import com.lizongying.mytv.api.LiveInfoRequest
@@ -43,54 +45,6 @@ class Request {
private val input =
"""{"mver":"1","subver":"1.2","host":"www.yangshipin.cn/#/tv/home?pid=","referer":"","canvas":"YSPANGLE(Apple,ANGLEMetalRenderer:AppleM1Pro,UnspecifiedVersion)"}""".toByteArray()
private var mapping = mapOf(
"CCTV4K" to "CCTV4K 超高清",
"CCTV1" to "CCTV1 综合",
"CCTV2" to "CCTV2 财经",
"CCTV4" to "CCTV4 中文国际",
"CCTV5" to "CCTV5 体育",
"CCTV5+" to "CCTV5+ 体育赛事",
"CCTV7" to "CCTV7 国防军事",
"CCTV9" to "CCTV9 记录",
"CCTV10" to "CCTV10 科教",
"CCTV11" to "CCTV11 戏曲",
"CCTV12" to "CCTV12 社会与法",
"CCTV13" to "CCTV13 新闻",
"CCTV14" to "CCTV14 少儿",
"CCTV15" to "CCTV15 音乐",
"CCTV16-HD" to "CCTV16 奥林匹克",
"CCTV17" to "CCTV17 农业农村",
"CGTN" to "CGTN",
"CGTN法语频道" to "CGTN 法语频道",
"CGTN俄语频道" to "CGTN 俄语频道",
"CGTN阿拉伯语频道" to "CGTN 阿拉伯语频道",
"CGTN西班牙语频道" to "CGTN 西班牙语频道",
"CGTN外语纪录频道" to "CGTN 纪录频道",
"东方卫视" to "东方卫视",
"湖南卫视" to "湖南卫视",
"湖北卫视" to "湖北卫视",
"辽宁卫视" to "辽宁卫视",
"江苏卫视" to "江苏卫视",
"江西卫视" to "江西卫视",
"山东卫视" to "山东卫视",
"广东卫视" to "广东卫视",
"广西卫视" to "广西卫视",
"重庆卫视" to "重庆卫视",
"河南卫视" to "河南卫视",
"河北卫视" to "河北卫视",
"贵州卫视" to "贵州卫视",
"北京卫视" to "北京卫视",
"黑龙江卫视" to "黑龙江卫视",
"浙江卫视" to "浙江卫视",
"安徽卫视" to "安徽卫视",
"深圳卫视" to "深圳卫视",
"四川卫视" to "四川卫视",
"福建东南卫视" to "东南卫视",
"海南卫视" to "海南卫视",
"天津卫视" to "天津卫视",
"新疆卫视" to "新疆卫视",
)
init {
handler.post(tokenRunnable)
}
@@ -100,6 +54,84 @@ class Request {
}
var call: Call<LiveInfo>? = null
var callAuth: Call<Auth>? = null
private fun fetchAuth(tvModel: TVViewModel, cookie: String) {
callAuth?.cancel()
val title = tvModel.title.value
val data = ysp?.getAuthData(tvModel)
val request = data?.let { AuthRequest(it) }
callAuth = request?.let { yspApiService.getAuth("guid=${ysp?.getGuid()}; $cookie", it) }
callAuth?.enqueue(object : Callback<Auth> {
override fun onResponse(call: Call<Auth>, response: Response<Auth>) {
if (response.isSuccessful) {
val liveInfo = response.body()
if (liveInfo?.data?.token != null) {
Log.i(TAG, "token ${liveInfo.data.token}")
ysp?.token = liveInfo.data.token
fetchVideo(tvModel, cookie)
} else {
Log.e(TAG, "$title token error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) {
if (!tvModel.getTV().mustToken) {
fetchAuth(tvModel, cookie)
}
} else {
token = ""
fetchAuth(tvModel)
}
} else {
fetchAuth(tvModel, cookie)
}
}
}
} else {
Log.e(TAG, "$title auth status error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) {
if (!tvModel.getTV().mustToken) {
fetchAuth(tvModel, cookie)
}
} else {
token = ""
fetchAuth(tvModel)
}
} else {
fetchAuth(tvModel, cookie)
}
}
}
}
override fun onFailure(call: Call<Auth>, t: Throwable) {
Log.e(TAG, "$title auth request error $t")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) {
if (!tvModel.getTV().mustToken) {
fetchVideo(tvModel, cookie)
}
} else {
token = ""
fetchAuth(tvModel)
}
} else {
fetchAuth(tvModel, cookie)
}
}
}
})
}
fun fetchVideo(tvModel: TVViewModel, cookie: String) {
call?.cancel()
@@ -112,7 +144,7 @@ class Request {
tvModel.seq = 0
val data = ysp?.switch(tvModel)
val request = data?.let { LiveInfoRequest(it) }
call = request?.let { yspApiService.getLiveInfo("guid=${ysp?.getGuid()}; $cookie", it) }
call = request?.let { yspApiService.getLiveInfo("guid=${ysp?.getGuid()}; $cookie", ysp!!.token, it) }
call?.enqueue(object : Callback<LiveInfo> {
override fun onResponse(call: Call<LiveInfo>, response: Response<LiveInfo>) {
@@ -223,6 +255,53 @@ class Request {
})
}
fun fetchAuth(tvModel: TVViewModel) {
if (token == "") {
yspTokenService.getInfo()
.enqueue(object : Callback<Info> {
override fun onResponse(call: Call<Info>, response: Response<Info>) {
if (response.isSuccessful) {
token = response.body()?.data?.token!!
Log.i(TAG, "info success $token")
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109;yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchAuth(tvModel, cookie)
} else {
Log.e(TAG, "info status error")
if (tvModel.tokenRetryTimes < tvModel.tokenRetryMaxTimes) {
tvModel.tokenRetryTimes++
fetchAuth(tvModel)
} else {
if (!tvModel.getTV().mustToken) {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109"
fetchAuth(tvModel, cookie)
}
}
}
}
override fun onFailure(call: Call<Info>, t: Throwable) {
Log.e(TAG, "info request error $t")
if (tvModel.tokenRetryTimes < tvModel.tokenRetryMaxTimes) {
tvModel.tokenRetryTimes++
fetchVideo(tvModel)
} else {
if (!tvModel.getTV().mustToken) {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109"
fetchAuth(tvModel, cookie)
}
}
}
})
} else {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109;yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchAuth(tvModel, cookie)
}
}
fun fetchVideo(tvModel: TVViewModel) {
if (token == "") {
yspTokenService.getInfo()
@@ -232,7 +311,7 @@ class Request {
token = response.body()?.data?.token!!
Log.i(TAG, "info success $token")
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; yspappid=519748109;yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109;yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchVideo(tvModel, cookie)
} else {
Log.e(TAG, "info status error")
@@ -242,7 +321,7 @@ class Request {
} else {
if (!tvModel.getTV().mustToken) {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; yspappid=519748109"
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109"
fetchVideo(tvModel, cookie)
}
}
@@ -257,7 +336,7 @@ class Request {
} else {
if (!tvModel.getTV().mustToken) {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; yspappid=519748109"
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109"
fetchVideo(tvModel, cookie)
}
}
@@ -265,18 +344,20 @@ class Request {
})
} else {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; yspappid=519748109;yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109;yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchVideo(tvModel, cookie)
}
}
fun fetchData(tvModel: TVViewModel) {
if (tvModel.getTV().needToken) {
fetchVideo(tvModel)
// fetchVideo(tvModel)
fetchAuth(tvModel)
} else {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; yspappid=519748109"
fetchVideo(tvModel, cookie)
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205"
// fetchVideo(tvModel, cookie)
fetchAuth(tvModel, cookie)
}
}
@@ -364,7 +445,6 @@ class Request {
for ((_, v) in TVList.list) {
for (v2 in v) {
Log.i(TAG, "$v2")
if (v2.title == item.channelName || v2.alias == item.channelName) {
v2.pid = item.pid
v2.sid = item.streamId
@@ -1,22 +1,28 @@
package com.lizongying.mytv
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.RequiresApi
import androidx.fragment.app.DialogFragment
import com.lizongying.mytv.databinding.DialogBinding
class SettingFragment(private val versionName: String,
private val channelReversal: Boolean,
private val channelNum: Boolean,
) :
class SettingFragment(
private val versionName: String,
private val versionCode: Long,
private val channelReversal: Boolean,
private val channelNum: Boolean,
) :
DialogFragment() {
private var _binding: DialogBinding? = null
private val binding get() = _binding!!
private lateinit var updateManager: UpdateManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
@@ -43,14 +49,33 @@ class SettingFragment(private val versionName: String,
(activity as MainActivity).saveChannelNum(isChecked)
}
updateManager = UpdateManager(context, this, versionCode)
_binding?.checkVersion?.setOnClickListener(OnClickListenerCheckVersion(updateManager))
return binding.root
}
fun setVersionName(versionName: String) {
binding.versionName.text = versionName
}
internal class OnClickListenerCheckVersion(private val updateManager: UpdateManager) :
View.OnClickListener {
override fun onClick(view: View?) {
updateManager.checkAndUpdate()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onDestroy() {
super.onDestroy()
// updateManager.destroy()
}
companion object {
const val TAG = "SettingFragment"
}
@@ -84,8 +84,8 @@ object TVList {
0,
"央视频道",
"https://resources.yangshipin.cn/assets/oms/image/202306/741515efda91f03f455df8a7da4ee11fa9329139c276435cf0a9e2af398d5bf2.png?imageMogr2/format/webp",
"600001802",
"2022574301",
"600108442",
"2013693901",
"600001802",
true,
mustToken = true
@@ -852,10 +852,24 @@ object TVList {
true,
mustToken = false
),
TV(
56,
"兵团卫视",
"兵团卫视",
listOf(),
0,
"地方频道",
"",
"600170344",
"2022606701",
"",
true,
mustToken = false
),
),
"国际频道" to listOf(
TV(
56,
57,
"CGTN",
"CGTN",
listOf("http://live.cgtn.com/1000/prog_index.m3u8"),
@@ -869,7 +883,7 @@ object TVList {
mustToken = false
),
TV(
57,
58,
"CGTN 法语频道",
"CGTN法语频道",
listOf("https://livefr.cgtn.com/1000f/prog_index.m3u8"),
@@ -883,7 +897,7 @@ object TVList {
mustToken = false
),
TV(
58,
59,
"CGTN 俄语频道",
"CGTN俄语频道",
listOf("http://liveru.cgtn.com/1000r/prog_index.m3u8"),
@@ -897,7 +911,7 @@ object TVList {
mustToken = false
),
TV(
59,
60,
"CGTN 阿拉伯语频道",
"CGTN阿拉伯语频道",
listOf("http://livear.cgtn.com/1000a/prog_index.m3u8"),
@@ -911,7 +925,7 @@ object TVList {
mustToken = false
),
TV(
60,
61,
"CGTN 西班牙语频道",
"CGTN西班牙语频道",
listOf(
@@ -928,7 +942,7 @@ object TVList {
mustToken = false
),
TV(
61,
62,
"CGTN 纪录频道",
"CGTN外语纪录频道",
listOf("https://livedoc.cgtn.com/500d/prog_index.m3u8"),
@@ -0,0 +1,189 @@
package com.lizongying.mytv
import android.app.DownloadManager
import android.app.DownloadManager.Request
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Toast
import com.lizongying.mytv.api.Release
import com.lizongying.mytv.requests.MyRequest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
class UpdateManager(
private var context: Context?,
private var settingFragment: SettingFragment,
private var versionCode: Long
) :
ConfirmationDialogFragment.ConfirmationDialogListener {
private var myRequest = MyRequest()
private var release: Release? = null
private var downloadReceiver: DownloadReceiver? = null
fun checkAndUpdate() {
CoroutineScope(Dispatchers.Main).launch {
try {
release = myRequest.getRelease()
updateUI(release)
Log.i(TAG, "versionCode $versionCode ${release?.data?.versionCode}")
if (release != null) {
if (release?.data?.versionCode!! >= versionCode) {
val dialog = ConfirmationDialogFragment(this@UpdateManager)
dialog.show(settingFragment.fragmentManager, "ConfirmationDialogFragment")
} else {
Toast.makeText(context, "不需要更新", Toast.LENGTH_LONG)
.show()
}
}
} catch (e: Exception) {
Log.e(TAG, "Error occurred: ${e.message}", e)
}
}
}
private fun updateUI(release: Release?) {
if (release?.data?.versionName.isNullOrEmpty()) {
settingFragment.setVersionName("版本获取失败")
} else {
settingFragment.setVersionName("最新版本:${release?.data?.versionName!!}")
}
}
private fun startDownload(release: Release) {
val apkFileName = "my-tv-${release.data.versionName}.apk"
Log.i(TAG, "apkFileName $apkFileName")
val downloadManager =
context!!.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = Request(Uri.parse(release.data.downloadUrl))
Log.i(TAG, "url ${Uri.parse(release.data.downloadUrl)}")
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, apkFileName)
request.setTitle("New Version Download")
request.setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
// 获取下载任务的引用
val downloadReference = downloadManager.enqueue(request)
downloadReceiver = DownloadReceiver(context!!, apkFileName, downloadReference)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context!!.registerReceiver(
downloadReceiver,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
Context.RECEIVER_NOT_EXPORTED,
)
} else {
context!!.registerReceiver(
downloadReceiver,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
)
}
getDownloadProgress(context!!, downloadReference) { progress ->
println("Download progress: $progress%")
}
}
private fun getDownloadProgress(context: Context, downloadId: Long, progressListener: (Int) -> Unit) {
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val handler = Handler(Looper.getMainLooper())
val intervalMillis: Long = 1000
handler.post(object : Runnable {
override fun run() {
Log.i(TAG, "search")
val query = DownloadManager.Query().setFilterById(downloadId)
val cursor: Cursor = downloadManager.query(query)
cursor.use {
if (it.moveToFirst()) {
val bytesDownloadedIndex =
it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
val bytesTotalIndex =
it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
// 检查列名是否存在
if (bytesDownloadedIndex != -1 && bytesTotalIndex != -1) {
val bytesDownloaded = it.getInt(bytesDownloadedIndex)
val bytesTotal = it.getInt(bytesTotalIndex)
if (bytesTotal != -1) {
val progress = (bytesDownloaded * 100L / bytesTotal).toInt()
progressListener(progress)
if (progress == 100) {
return
}
}
}
}
}
// handler.postDelayed(this, intervalMillis)
}
})
}
private class DownloadReceiver(
private val context: Context,
private val apkFileName: String,
private val downloadReference: Long
) : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val reference = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
Log.i(TAG, "reference $reference")
val progress = intent.getIntExtra("progress", 0)
Log.i(TAG, "progress $progress")
// 检查是否是我们发起的下载
if (reference == downloadReference) {
// 下载完成,触发安装
installNewVersion()
}
}
private fun installNewVersion() {
val installIntent = Intent(Intent.ACTION_VIEW)
val apkUri = Uri.fromFile(
File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
apkFileName
)
)
installIntent.setDataAndType(apkUri, "application/vnd.android.package-archive")
installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(installIntent)
}
}
companion object {
private const val TAG = "UpdateManager"
}
override fun onConfirm() {
Log.i(TAG, "onConfirm $release")
release?.let { startDownload(it) }
}
override fun onCancel() {
}
fun destroy() {
if (downloadReceiver != null) {
context!!.unregisterReceiver(downloadReceiver)
Log.i(TAG, "destroy downloadReceiver")
}
}
}
+49 -4
View File
@@ -3,18 +3,63 @@ package com.lizongying.mytv
import android.content.Context
import android.content.res.Resources
import android.util.TypedValue
import com.google.gson.Gson
import com.lizongying.mytv.api.TimeResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
object Utils {
private var between: Long = 0
fun getDateFormat(format: String): String {
return SimpleDateFormat(format, Locale.CHINA).format(Date())
return SimpleDateFormat(
format,
Locale.CHINA
).format(Date(System.currentTimeMillis() - between))
}
fun getDateTimestamp(): Long {
return Date().time / 1000
return (System.currentTimeMillis() - between) / 1000
}
suspend fun init() {
var currentTimeMillis: Long = 0
try {
currentTimeMillis = getTimestampFromServer()
} catch (e: Exception) {
println("Failed to retrieve timestamp from server: ${e.message}")
}
between = System.currentTimeMillis() - currentTimeMillis
}
/**
* 从服务器获取时间戳
* @return Long 时间戳
*/
private suspend fun getTimestampFromServer(): Long {
return withContext(Dispatchers.IO) {
val client = okhttp3.OkHttpClient.Builder()
.connectTimeout(500, java.util.concurrent.TimeUnit.MILLISECONDS)
.readTimeout(1, java.util.concurrent.TimeUnit.SECONDS).build()
val request = okhttp3.Request.Builder()
.url("https://api.m.taobao.com/rest/api3.do?api=mtop.common.getTimestamp")
.build()
try {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val string = response.body()?.string()
Gson().fromJson(string, TimeResponse::class.java).data.t.toLong()
}
} catch (e: IOException) {
// Handle network errors
throw IOException("Error during network request", e)
}
}
}
fun dpToPx(dp: Float): Int {
@@ -5,6 +5,8 @@ import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.protobuf.ProtoConverterFactory
import java.net.InetSocketAddress
import java.net.Proxy
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
@@ -13,9 +15,11 @@ import javax.net.ssl.X509TrustManager
class ApiClient {
private val yspUrl = "https://player-api.yangshipin.cn/"
private val myUrl = "https://lyrics.run/"
private val devUrl = "http://10.0.2.2:8081/"
private val protoUrl = "https://capi.yangshipin.cn/"
private val traceUrl = "https://btrace.yangshipin.cn/"
private var okHttpClient = getUnsafeOkHttpClient()
val yspApiService: YSPApiService by lazy {
@@ -34,6 +38,14 @@ class ApiClient {
.build().create(YSPTokenService::class.java)
}
val releaseService: ReleaseService by lazy {
Retrofit.Builder()
.baseUrl(devUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build().create(ReleaseService::class.java)
}
val yspProtoService: YSPProtoService by lazy {
Retrofit.Builder()
.baseUrl(protoUrl)
@@ -0,0 +1,37 @@
package com.lizongying.mytv.api
import android.util.Log
import okhttp3.MediaType
import okhttp3.RequestBody
import okio.BufferedSink
import java.io.IOException
data class Auth(
val code: Int,
val msg: String,
val data: AuthData,
)
data class AuthData(
val token: String,
)
data class AuthRequest(
var data: String,
) : RequestBody() {
override fun contentType(): MediaType? {
return MediaType.parse("application/x-www-form-urlencoded;charset=UTF-8")
}
override fun writeTo(sink: BufferedSink) {
try {
sink.writeUtf8(data)
} catch (e: IOException) {
Log.e(TAG, "$e")
}
}
companion object {
private const val TAG = "AuthRequest"
}
}
@@ -3,9 +3,30 @@ package com.lizongying.mytv.api
data class Info(
val code: Int?,
val msg: String?,
val data: InfoData,
)
val data: Data,
) {
data class Data(
val token: String,
)
}
data class InfoData(
val token: String,
)
data class Release(
val code: Int?,
val msg: String?,
val data: Data,
) {
data class Data(
val versionName: String,
val versionCode: Int,
val downloadUrl: String,
val updateTime: Int,
)
}
data class TimeResponse(
val data: Time
) {
data class Time(
val t: String
)
}
@@ -0,0 +1,11 @@
package com.lizongying.mytv.api
import retrofit2.Call
import retrofit2.http.GET
interface ReleaseService {
@GET("my-tv/v1/release")
fun getRelease(
): Call<Release>
}
@@ -50,6 +50,9 @@ class YSP(var context: Context) {
private var signature = ""
private var appid = "ysp_pc"
var token = ""
private var encryptor: Encryptor? = null
private lateinit var sharedPref: SharedPreferences
@@ -83,6 +86,19 @@ class YSP(var context: Context) {
return """{"cnlid":"$cnlid","livepid":"$livepid","stream":"$stream","guid":"$guid","cKey":"$cKey","adjust":$adjust,"sphttps":"$sphttps","platform":"$platform","cmd":"$cmd","encryptVer":"$encryptVer","dtype":"$dtype","devid":"$devid","otype":"$otype","appVer":"$appVer","app_version":"$appVersion","rand_str":"$randStr","channel":"$channel","defn":"$defn","signature":"$signature"}"""
}
fun getAuthData(tvModel: TVViewModel): String {
livepid = tvModel.pid.value!!
randStr = getRand()
if (tvModel.retryTimes > 0) {
guid = newGuid()
}
signature = getAuthSignature()
return """pid=$livepid&guid=$guid&appid=$appid&rand_str=$randStr&signature=$signature"""
}
private fun getTimeStr(): String {
return getDateTimestamp().toString()
}
@@ -131,6 +147,13 @@ class YSP(var context: Context) {
return hashedData.let { it -> it.joinToString("") { "%02x".format(it) } }
}
private fun getAuthSignature(): String {
val e =
"appid=${appid}&guid=${guid}&pid=${livepid}&rand_str=${randStr}".toByteArray()
val hashedData = encryptor?.hash2(e) ?: return ""
return hashedData.let { it -> it.joinToString("") { "%02x".format(it) } }
}
companion object {
private const val TAG = "YSP"
}
@@ -13,9 +13,23 @@ interface YSPApiService {
"content-type: application/json;charset=UTF-8",
"referer: https://www.yangshipin.cn/",
"yspappid: 519748109",
"user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
)
fun getLiveInfo(
@Header("cookie") cookie: String,
@Header("Yspplayertoken") token: String,
@Body request: LiveInfoRequest,
): Call<LiveInfo>
@POST("v1/player/auth")
@Headers(
"content-type: application/x-www-form-urlencoded;charset=UTF-8",
"referer: https://www.yangshipin.cn/",
"yspappid: 519748109",
"user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
)
fun getAuth(
@Header("cookie") cookie: String,
@Body request: AuthRequest,
): Call<Auth>
}
@@ -0,0 +1,44 @@
package com.lizongying.mytv.requests
import com.lizongying.mytv.api.ApiClient
import com.lizongying.mytv.api.Release
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class MyRequest {
private var releaseService = ApiClient().releaseService
suspend fun getRelease(): Release? {
return withContext(Dispatchers.IO) {
fetchRelease()
}
}
private suspend fun fetchRelease(): Release? {
return suspendCoroutine { continuation ->
releaseService.getRelease()
.enqueue(object : Callback<Release> {
override fun onResponse(call: Call<Release>, response: Response<Release>) {
if (response.isSuccessful) {
continuation.resume(response.body())
} else {
continuation.resume(null)
}
}
override fun onFailure(call: Call<Release>, t: Throwable) {
continuation.resume(null)
}
})
}
}
companion object {
private const val TAG = "MyRequest"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+25 -1
View File
@@ -27,7 +27,31 @@
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/"
></TextView>
/>
<LinearLayout
android:layout_width="300dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<Button
android:id="@+id/check_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="@string/check_version"
/>
<ProgressBar
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/version_name"
android:text=""
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<Switch
android:id="@+id/switch_channel_reversal"
android:text="@string/title_channel_reversal"
+1
View File
@@ -2,4 +2,5 @@
<string name="app_name">我的电视</string>
<string name="title_channel_reversal">换台反转</string>
<string name="title_channel_num">数字选台</string>
<string name="check_version">检查更新</string>
</resources>