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
import android.app.AlertDialog
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.Signature
@@ -15,11 +16,7 @@ import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
import android.view.WindowManager
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import com.lizongying.mytv.models.TVViewModel
import java.security.MessageDigest
@@ -36,7 +33,10 @@ class MainActivity : FragmentActivity() {
private lateinit var gestureDetector: GestureDetector
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?) {
super.onCreate(savedInstanceState)
@@ -56,6 +56,9 @@ class MainActivity : FragmentActivity() {
mainFragment.view?.requestFocus()
}
gestureDetector = GestureDetector(this, GestureListener())
sharedPref = getPreferences(Context.MODE_PRIVATE)
channelReversal = sharedPref.getBoolean(CHANNEL_REVERSAL, false)
}
fun showInfoFragment(tvViewModel: TVViewModel) {
@@ -63,7 +66,6 @@ class MainActivity : FragmentActivity() {
}
fun play(tvViewModel: TVViewModel) {
Log.i(TAG, "play: ${tvViewModel.getTV()}")
playerFragment.play(tvViewModel)
mainFragment.view?.requestFocus()
}
@@ -170,88 +172,135 @@ class MainActivity : FragmentActivity() {
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) {
KeyEvent.KEYCODE_BACK -> {
if (!mainFragmentIsHidden()) {
hideMainFragment()
return true
}
fun saveChannelReversal(channelReversal: Boolean) {
with(sharedPref.edit()) {
putBoolean(CHANNEL_REVERSAL, channelReversal)
apply()
}
this.channelReversal = channelReversal
}
if (doubleBackToExitPressedOnce) {
super.onBackPressed()
return true
}
private fun showHelp() {
if (!mainFragment.isHidden) {
return
}
this.doubleBackToExitPressedOnce = true
Toast.makeText(this, "再按一次退出", Toast.LENGTH_SHORT).show()
val versionName = getPackageInfo().versionName
val dialogFragment = MyDialogFragment(versionName, channelReversal)
dialogFragment.show(supportFragmentManager, "settings_dialog")
}
Handler(Looper.getMainLooper()).postDelayed({
doubleBackToExitPressedOnce = false
}, 2000)
return true
private fun channelUp() {
if (mainFragment.isHidden) {
if (channelReversal) {
next()
return
}
KeyEvent.KEYCODE_MENU -> {
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 {
prev()
} else {
// if (mainFragment.selectedPosition == 0) {
// mainFragment.setSelectedPosition(
// mainFragment.tvListViewModel.maxNum.size - 1,
// false
// )
// }
}
}
}
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
if (mainFragment.isHidden) {
next()
} else {
private fun channelDown() {
if (mainFragment.isHidden) {
if (channelReversal) {
prev()
return
}
next()
} else {
// if (mainFragment.selectedPosition == mainFragment.tvListViewModel.maxNum.size - 1) {
//// mainFragment.setSelectedPosition(0, false)
// hideMainFragment()
// 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 -> {
@@ -336,5 +385,6 @@ class MainActivity : FragmentActivity() {
companion object {
private const val TAG = "MainActivity"
private const val CHANNEL_REVERSAL = "channel_reversal"
}
}
@@ -209,8 +209,8 @@ class MainFragment : BrowseSupportFragment() {
itemPosition = sharedPref?.getInt("position", 0)!!
if (itemPosition >= tvListViewModel.size()) {
itemPosition = 0
tvListViewModel.setItemPosition(itemPosition)
}
tvListViewModel.setItemPosition(itemPosition)
}
fun fragmentReady() {
@@ -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)
}
var call: Call<LiveInfo>? = null
fun fetchVideo(tvModel: TVViewModel, cookie: String) {
call?.cancel()
if (::myRunnable.isInitialized) {
handler.removeCallbacks(myRunnable)
}
@@ -97,71 +100,71 @@ 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) }
request?.let { yspApiService.getLiveInfo(cookie, it) }
?.enqueue(object : Callback<LiveInfo> {
override fun onResponse(call: Call<LiveInfo>, response: Response<LiveInfo>) {
if (response.isSuccessful) {
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 regex = Regex("""des_key = "([^"]+).+var des_iv = "([^"]+)""")
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()
Log.i(TAG, "$title url $url")
tvModel.addVideoUrl(url)
tvModel.allReady()
tvModel.retryTimes = 0
myRunnable = MyRunnable(tvModel)
handler.post(myRunnable)
} else {
Log.e(TAG, "$title key error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
}
call?.enqueue(object : Callback<LiveInfo> {
override fun onResponse(call: Call<LiveInfo>, response: Response<LiveInfo>) {
if (response.isSuccessful) {
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 regex = Regex("""des_key = "([^"]+).+var des_iv = "([^"]+)""")
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()
Log.i(TAG, "$title url $url")
tvModel.addVideoUrl(url)
tvModel.allReady()
tvModel.retryTimes = 0
myRunnable = MyRunnable(tvModel)
handler.post(myRunnable)
} else {
if (liveInfo?.data?.errinfo != null && liveInfo.data.errinfo == "应版权方要求,暂停提供直播信号,请点击观看其他精彩节目") {
Log.e(TAG, "$title error ${liveInfo.data.errinfo}")
tvModel.setErrInfo(liveInfo.data.errinfo)
} else {
Log.e(TAG, "$title url error $request $liveInfo")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
Log.e(TAG, "$title key error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
}
} else {
Log.e(TAG, "$title status error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
if (liveInfo?.data?.errinfo != null && liveInfo.data.errinfo == "应版权方要求,暂停提供直播信号,请点击观看其他精彩节目") {
Log.e(TAG, "$title error ${liveInfo.data.errinfo}")
tvModel.setErrInfo(liveInfo.data.errinfo)
} else {
Log.e(TAG, "$title url error $request $liveInfo")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
}
}
}
override fun onFailure(call: Call<LiveInfo>, t: Throwable) {
Log.e(TAG, "$title request error")
} else {
Log.e(TAG, "$title status error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
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) {
@@ -172,7 +175,7 @@ class Request {
val token = response.body()?.data?.token
Log.i(TAG, "info success $token")
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)
} else {
Log.e(TAG, "info status error")
@@ -197,7 +200,7 @@ class Request {
if (tvModel.needToken) {
fetchVideo(tvModel)
} else {
val cookie = "guid=1; vplatform=109"
val cookie = "vplatform=109"
fetchVideo(tvModel, cookie)
}
}
@@ -5,6 +5,9 @@ import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.protobuf.ProtoConverterFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
class ApiClient {
@@ -13,9 +16,7 @@ class ApiClient {
private val protoUrl = "https://capi.yangshipin.cn/"
private val traceUrl = "https://btrace.yangshipin.cn/"
private var okHttpClient = OkHttpClient.Builder()
.dns(DnsCache())
.build()
private var okHttpClient = getUnsafeOkHttpClient()
val yspApiService: YSPApiService by lazy {
Retrofit.Builder()
@@ -48,4 +49,40 @@ class ApiClient {
.addConverterFactory(GsonConverterFactory.create())
.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()
guid = newGuid()
if (tvModel.retryTimes > 0) {
guid = newGuid()
}
timeStr = getTimeStr()
// guid = "lq3oqitm_1e15dnzgjnb"
@@ -107,7 +110,7 @@ class YSP(var context: Context) {
return guid
}
fun newGuid(): String {
private fun newGuid(): String {
guid = generateGuid()
with(sharedPref!!.edit()) {
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_height="wrap_content"
android:layout_marginTop="8dp"
android:contentDescription="@string/logo"
android:padding="10dp" />
<LinearLayout
+1 -1
View File
@@ -1,4 +1,4 @@
<resources>
<string name="app_name">我的电视</string>
<string name="logo">logo</string>
<string name="title_channel_reversal">频道反转</string>
</resources>
-1
View File
@@ -1,6 +1,5 @@
<resources>
<style name="CustomImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle">
<item name="lbImageCardViewType">Title|Content</item>
</style>