Compare commits

...
5 Commits
Author SHA1 Message Date
Li ZongYing 1fd0951f92 channel reversal 2024-01-16 19:04:43 +08:00
Li ZongYing 42b072f5ec support KEYCODE_CHANNEL_UP & KEYCODE_CHANNEL_DOWN 2024-01-14 12:09:12 +08:00
Li ZongYing a23b1e11ce fix the menu did not appear 2024-01-14 10:15:45 +08:00
Li ZongYing 1ac6d87c3c clean log 2024-01-12 10:13:29 +08:00
Li ZongYing b075a7db81 trust all certs 2024-01-11 19:33:31 +08:00
10 changed files with 316 additions and 137 deletions
@@ -1,6 +1,7 @@
package com.lizongying.mytv package com.lizongying.mytv
import android.app.AlertDialog import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.Signature import android.content.pm.Signature
@@ -15,11 +16,7 @@ import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION import android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
import android.view.WindowManager import android.view.WindowManager
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.lizongying.mytv.models.TVViewModel import com.lizongying.mytv.models.TVViewModel
import java.security.MessageDigest import java.security.MessageDigest
@@ -36,7 +33,10 @@ class MainActivity : FragmentActivity() {
private lateinit var gestureDetector: GestureDetector private lateinit var gestureDetector: GestureDetector
private val handler = Handler() private val handler = Handler()
private val delay: Long = 3000 private val delay: Long = 4000
private lateinit var sharedPref: SharedPreferences
private var channelReversal = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -56,6 +56,9 @@ class MainActivity : FragmentActivity() {
mainFragment.view?.requestFocus() mainFragment.view?.requestFocus()
} }
gestureDetector = GestureDetector(this, GestureListener()) gestureDetector = GestureDetector(this, GestureListener())
sharedPref = getPreferences(Context.MODE_PRIVATE)
channelReversal = sharedPref.getBoolean(CHANNEL_REVERSAL, false)
} }
fun showInfoFragment(tvViewModel: TVViewModel) { fun showInfoFragment(tvViewModel: TVViewModel) {
@@ -63,7 +66,6 @@ class MainActivity : FragmentActivity() {
} }
fun play(tvViewModel: TVViewModel) { fun play(tvViewModel: TVViewModel) {
Log.i(TAG, "play: ${tvViewModel.getTV()}")
playerFragment.play(tvViewModel) playerFragment.play(tvViewModel)
mainFragment.view?.requestFocus() mainFragment.view?.requestFocus()
} }
@@ -170,88 +172,135 @@ class MainActivity : FragmentActivity() {
} }
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { fun saveChannelReversal(channelReversal: Boolean) {
when (keyCode) { with(sharedPref.edit()) {
KeyEvent.KEYCODE_BACK -> { putBoolean(CHANNEL_REVERSAL, channelReversal)
if (!mainFragmentIsHidden()) { apply()
hideMainFragment() }
return true this.channelReversal = channelReversal
} }
if (doubleBackToExitPressedOnce) { private fun showHelp() {
super.onBackPressed() if (!mainFragment.isHidden) {
return true return
} }
this.doubleBackToExitPressedOnce = true val versionName = getPackageInfo().versionName
Toast.makeText(this, "再按一次退出", Toast.LENGTH_SHORT).show() val dialogFragment = MyDialogFragment(versionName, channelReversal)
dialogFragment.show(supportFragmentManager, "settings_dialog")
}
Handler(Looper.getMainLooper()).postDelayed({ private fun channelUp() {
doubleBackToExitPressedOnce = false if (mainFragment.isHidden) {
}, 2000) if (channelReversal) {
return true next()
return
} }
prev()
KeyEvent.KEYCODE_MENU -> { } else {
val versionName = getPackageInfo().versionName
val textView = TextView(this)
textView.text =
"当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/"
val imageView = ImageView(this)
val drawable = ContextCompat.getDrawable(this, R.drawable.appreciate)
imageView.setImageDrawable(drawable)
val linearLayout = LinearLayout(this)
linearLayout.orientation = LinearLayout.VERTICAL
linearLayout.addView(textView)
linearLayout.addView(imageView)
val layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
imageView.layoutParams = layoutParams
textView.layoutParams = layoutParams
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder
.setView(linearLayout)
val dialog: AlertDialog = builder.create()
dialog.show()
return true
}
KeyEvent.KEYCODE_DPAD_CENTER -> {
Log.i(TAG, "KEYCODE_DPAD_CENTER")
switchMainFragment()
}
KeyEvent.KEYCODE_DPAD_UP -> {
if (mainFragment.isHidden) {
prev()
} else {
// if (mainFragment.selectedPosition == 0) { // if (mainFragment.selectedPosition == 0) {
// mainFragment.setSelectedPosition( // mainFragment.setSelectedPosition(
// mainFragment.tvListViewModel.maxNum.size - 1, // mainFragment.tvListViewModel.maxNum.size - 1,
// false // false
// ) // )
// } // }
} }
} }
KeyEvent.KEYCODE_DPAD_DOWN -> { private fun channelDown() {
if (mainFragment.isHidden) { if (mainFragment.isHidden) {
next() if (channelReversal) {
} else { prev()
return
}
next()
} else {
// if (mainFragment.selectedPosition == mainFragment.tvListViewModel.maxNum.size - 1) { // if (mainFragment.selectedPosition == mainFragment.tvListViewModel.maxNum.size - 1) {
//// mainFragment.setSelectedPosition(0, false) //// mainFragment.setSelectedPosition(0, false)
// hideMainFragment() // hideMainFragment()
// return false // return false
// } // }
} }
}
private fun back() {
if (!mainFragmentIsHidden()) {
hideMainFragment()
return
}
if (doubleBackToExitPressedOnce) {
super.onBackPressed()
return
}
doubleBackToExitPressedOnce = true
Toast.makeText(this, "再按一次退出", Toast.LENGTH_SHORT).show()
Handler(Looper.getMainLooper()).postDelayed({
doubleBackToExitPressedOnce = false
}, 2000)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) {
KeyEvent.KEYCODE_ESCAPE -> {
back()
return true
}
KeyEvent.KEYCODE_BACK -> {
back()
return true
}
KeyEvent.KEYCODE_BOOKMARK -> {
showHelp()
return true
}
KeyEvent.KEYCODE_UNKNOWN -> {
showHelp()
return true
}
KeyEvent.KEYCODE_HELP -> {
showHelp()
return true
}
KeyEvent.KEYCODE_SETTINGS -> {
showHelp()
return true
}
KeyEvent.KEYCODE_MENU -> {
showHelp()
return true
}
KeyEvent.KEYCODE_ENTER -> {
switchMainFragment()
}
KeyEvent.KEYCODE_DPAD_CENTER -> {
switchMainFragment()
}
KeyEvent.KEYCODE_DPAD_UP -> {
channelUp()
}
KeyEvent.KEYCODE_CHANNEL_UP -> {
channelUp()
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
channelDown()
}
KeyEvent.KEYCODE_CHANNEL_DOWN -> {
channelDown()
} }
KeyEvent.KEYCODE_DPAD_LEFT -> { KeyEvent.KEYCODE_DPAD_LEFT -> {
@@ -336,5 +385,6 @@ class MainActivity : FragmentActivity() {
companion object { companion object {
private const val TAG = "MainActivity" private const val TAG = "MainActivity"
private const val CHANNEL_REVERSAL = "channel_reversal"
} }
} }
@@ -209,8 +209,8 @@ class MainFragment : BrowseSupportFragment() {
itemPosition = sharedPref?.getInt("position", 0)!! itemPosition = sharedPref?.getInt("position", 0)!!
if (itemPosition >= tvListViewModel.size()) { if (itemPosition >= tvListViewModel.size()) {
itemPosition = 0 itemPosition = 0
tvListViewModel.setItemPosition(itemPosition)
} }
tvListViewModel.setItemPosition(itemPosition)
} }
fun fragmentReady() { fun fragmentReady() {
@@ -0,0 +1,44 @@
package com.lizongying.mytv
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.DialogBinding
class MyDialogFragment(private val versionName: String, private val channelReversal: Boolean) :
DialogFragment() {
private var _binding: DialogBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = DialogBinding.inflate(inflater, container, false)
_binding?.version?.text =
"当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/"
val switchView = _binding?.switchView
switchView?.isChecked = channelReversal
switchView?.setOnCheckedChangeListener { _, isChecked ->
(activity as MainActivity).saveChannelReversal(isChecked)
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
const val TAG = "MyDialogFragment"
}
}
@@ -87,7 +87,10 @@ class Request {
ysp = YSP(context) ysp = YSP(context)
} }
var call: Call<LiveInfo>? = null
fun fetchVideo(tvModel: TVViewModel, cookie: String) { fun fetchVideo(tvModel: TVViewModel, cookie: String) {
call?.cancel()
if (::myRunnable.isInitialized) { if (::myRunnable.isInitialized) {
handler.removeCallbacks(myRunnable) handler.removeCallbacks(myRunnable)
} }
@@ -97,71 +100,71 @@ class Request {
tvModel.seq = 0 tvModel.seq = 0
val data = ysp?.switch(tvModel) val data = ysp?.switch(tvModel)
val request = data?.let { LiveInfoRequest(it) } val request = data?.let { LiveInfoRequest(it) }
call = request?.let { yspApiService.getLiveInfo("guid=${ysp?.getGuid()}; $cookie", it) }
request?.let { yspApiService.getLiveInfo(cookie, it) } call?.enqueue(object : Callback<LiveInfo> {
?.enqueue(object : Callback<LiveInfo> { override fun onResponse(call: Call<LiveInfo>, response: Response<LiveInfo>) {
override fun onResponse(call: Call<LiveInfo>, response: Response<LiveInfo>) { if (response.isSuccessful) {
if (response.isSuccessful) { val liveInfo = response.body()
val liveInfo = response.body() if (liveInfo?.data?.playurl != null) {
if (liveInfo?.data?.playurl != null) { val chanll = liveInfo.data.chanll
val chanll = liveInfo.data.chanll val decodedBytes = Base64.decode(
val decodedBytes = Base64.decode( chanll.substring(9, chanll.length - 3),
chanll.substring(9, chanll.length - 3), Base64.DEFAULT
Base64.DEFAULT )
) val decodedString = String(decodedBytes)
val decodedString = String(decodedBytes) val regex = Regex("""des_key = "([^"]+).+var des_iv = "([^"]+)""")
val regex = Regex("""des_key = "([^"]+).+var des_iv = "([^"]+)""") val matchResult = regex.find(decodedString)
val matchResult = regex.find(decodedString) if (matchResult != null) {
if (matchResult != null) { val (key, iv) = matchResult.destructured
val (key, iv) = matchResult.destructured val keyBytes = Base64.decode(key, Base64.DEFAULT)
val keyBytes = Base64.decode(key, Base64.DEFAULT) val ivBytes = Base64.decode(iv, Base64.DEFAULT)
val ivBytes = Base64.decode(iv, Base64.DEFAULT) val url = liveInfo.data.playurl + "&revoi=" + encryptTripleDES(
val url = liveInfo.data.playurl + "&revoi=" + encryptTripleDES( keyBytes + byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0),
keyBytes + byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0), ivBytes
ivBytes ).uppercase()
).uppercase() Log.i(TAG, "$title url $url")
Log.i(TAG, "$title url $url") tvModel.addVideoUrl(url)
tvModel.addVideoUrl(url) tvModel.allReady()
tvModel.allReady() tvModel.retryTimes = 0
tvModel.retryTimes = 0 myRunnable = MyRunnable(tvModel)
myRunnable = MyRunnable(tvModel) handler.post(myRunnable)
handler.post(myRunnable)
} else {
Log.e(TAG, "$title key error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
}
} else { } else {
if (liveInfo?.data?.errinfo != null && liveInfo.data.errinfo == "应版权方要求,暂停提供直播信号,请点击观看其他精彩节目") { Log.e(TAG, "$title key error")
Log.e(TAG, "$title error ${liveInfo.data.errinfo}") if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.setErrInfo(liveInfo.data.errinfo) tvModel.retryTimes++
} else { fetchVideo(tvModel, cookie)
Log.e(TAG, "$title url error $request $liveInfo")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
} }
} }
} else { } else {
Log.e(TAG, "$title status error") if (liveInfo?.data?.errinfo != null && liveInfo.data.errinfo == "应版权方要求,暂停提供直播信号,请点击观看其他精彩节目") {
if (tvModel.retryTimes < tvModel.retryMaxTimes) { Log.e(TAG, "$title error ${liveInfo.data.errinfo}")
tvModel.retryTimes++ tvModel.setErrInfo(liveInfo.data.errinfo)
fetchVideo(tvModel, cookie) } else {
Log.e(TAG, "$title url error $request $liveInfo")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
} }
} }
} } else {
Log.e(TAG, "$title status error")
override fun onFailure(call: Call<LiveInfo>, t: Throwable) {
Log.e(TAG, "$title request error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) { if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++ tvModel.retryTimes++
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
} }
} }
}) }
override fun onFailure(call: Call<LiveInfo>, t: Throwable) {
Log.e(TAG, "$title request error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
}
})
} }
fun fetchVideo(tvModel: TVViewModel) { fun fetchVideo(tvModel: TVViewModel) {
@@ -172,7 +175,7 @@ class Request {
val token = response.body()?.data?.token val token = response.body()?.data?.token
Log.i(TAG, "info success $token") Log.i(TAG, "info success $token")
val cookie = val cookie =
"guid=1; vplatform=109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token" "vplatform=109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
} else { } else {
Log.e(TAG, "info status error") Log.e(TAG, "info status error")
@@ -197,7 +200,7 @@ class Request {
if (tvModel.needToken) { if (tvModel.needToken) {
fetchVideo(tvModel) fetchVideo(tvModel)
} else { } else {
val cookie = "guid=1; vplatform=109" val cookie = "vplatform=109"
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
} }
} }
@@ -5,6 +5,9 @@ import okhttp3.OkHttpClient
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.protobuf.ProtoConverterFactory import retrofit2.converter.protobuf.ProtoConverterFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
class ApiClient { class ApiClient {
@@ -13,9 +16,7 @@ class ApiClient {
private val protoUrl = "https://capi.yangshipin.cn/" private val protoUrl = "https://capi.yangshipin.cn/"
private val traceUrl = "https://btrace.yangshipin.cn/" private val traceUrl = "https://btrace.yangshipin.cn/"
private var okHttpClient = OkHttpClient.Builder() private var okHttpClient = getUnsafeOkHttpClient()
.dns(DnsCache())
.build()
val yspApiService: YSPApiService by lazy { val yspApiService: YSPApiService by lazy {
Retrofit.Builder() Retrofit.Builder()
@@ -48,4 +49,40 @@ class ApiClient {
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build().create(YSPBtraceService::class.java) .build().create(YSPBtraceService::class.java)
} }
private fun getUnsafeOkHttpClient(): OkHttpClient {
try {
val trustAllCerts: Array<TrustManager> = arrayOf(
object : X509TrustManager {
override fun checkClientTrusted(
chain: Array<out java.security.cert.X509Certificate>?,
authType: String?
) {
}
override fun checkServerTrusted(
chain: Array<out java.security.cert.X509Certificate>?,
authType: String?
) {
}
override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> {
return emptyArray()
}
}
)
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
return OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager)
.hostnameVerifier { _, _ -> true }
.dns(DnsCache())
.build()
} catch (e: Exception) {
throw RuntimeException(e)
}
}
} }
@@ -71,7 +71,10 @@ class YSP(var context: Context) {
randStr = getRand() randStr = getRand()
guid = newGuid() if (tvModel.retryTimes > 0) {
guid = newGuid()
}
timeStr = getTimeStr() timeStr = getTimeStr()
// guid = "lq3oqitm_1e15dnzgjnb" // guid = "lq3oqitm_1e15dnzgjnb"
@@ -107,7 +110,7 @@ class YSP(var context: Context) {
return guid return guid
} }
fun newGuid(): String { private fun newGuid(): String {
guid = generateGuid() guid = generateGuid()
with(sharedPref!!.edit()) { with(sharedPref!!.edit()) {
putString("guid", guid) putString("guid", guid)
+44
View File
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="300dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="20dp"
android:paddingLeft="7dp"
android:paddingRight="7dp"
android:background="#FF263238"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textSize="16sp" />
<TextView
android:id="@+id/version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:text="当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/"
></TextView>
<Switch
android:id="@+id/switch_view"
android:text="@string/title_channel_reversal"
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>
-1
View File
@@ -12,7 +12,6 @@
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:contentDescription="@string/logo"
android:padding="10dp" /> android:padding="10dp" />
<LinearLayout <LinearLayout
+1 -1
View File
@@ -1,4 +1,4 @@
<resources> <resources>
<string name="app_name">我的电视</string> <string name="app_name">我的电视</string>
<string name="logo">logo</string> <string name="title_channel_reversal">频道反转</string>
</resources> </resources>
-1
View File
@@ -1,6 +1,5 @@
<resources> <resources>
<style name="CustomImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle"> <style name="CustomImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle">
<item name="lbImageCardViewType">Title|Content</item> <item name="lbImageCardViewType">Title|Content</item>
</style> </style>