diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 9d04ae6d..b15fc757 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -7,7 +7,7 @@ body: id: checklist attributes: label: 检查清单 - description: 声明:本项目仅为自用修改,仅在android4.4的夏普电视测试,作者没有任何义务为任何人解决问题或添加功能,如果你遇到了问题,建议 1)自行修改源代码解决; 2)使用AI如claude.ai协助解决; 3)向本项目原作者@lizhongying求助 + description: 声明:本项目仅为自用修改,仅在android4.4的夏普电视测试,作者没有任何义务为任何人解决问题或添加功能,如果你遇到了问题,建议 1)自行修改源代码解决; 2)使用AI如claude.ai协助解决; 3)向本项目原作者求助 options: - label: 明白上述声明 required: true diff --git a/.github/ISSUE_TEMPLATE/fr.yml b/.github/ISSUE_TEMPLATE/fr.yml index 8c388143..7fa96567 100644 --- a/.github/ISSUE_TEMPLATE/fr.yml +++ b/.github/ISSUE_TEMPLATE/fr.yml @@ -7,7 +7,7 @@ body: id: checklist attributes: label: 检查清单 - description: 声明:本项目仅为自用修改,仅在android4.4的夏普电视测试,作者没有任何义务为任何人解决问题或添加功能,如果你遇到了问题,建议 1)自行修改源代码解决; 2)使用AI如claude.ai协助解决; 3)向本项目原作者@lizhongying求助 + description: 声明:本项目仅为自用修改,仅在android4.4的夏普电视测试,作者没有任何义务为任何人解决问题或添加功能,如果你遇到了问题,建议 1)自行修改源代码解决; 2)使用AI如claude.ai协助解决; 3)向本项目原作者求助 options: - label: 明白上述声明 required: true diff --git a/HISTORY.md b/HISTORY.md index dfcf9ea0..ebdf80ff 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,8 +1,11 @@ ## 更新日志 -### v1.2.8-kk +### v1.2.8-ijk -* support android kitkat +* Use ijkplayer to get better playback effect on low-end devices. Please test it yourself and decide which version to use based on the effect. Thanks @caixxxin . +* Support android kitkat +* Improve https access module compatibility with android 4.4 +* Fixed a weird bug on Sharp TV: after the TV is started, this APP will start twice and stop playing after the second start. ### v1.2.8 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 57579cdc..be05805f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -103,4 +103,7 @@ dependencies { implementation(libs.recyclerview) implementation(files("libs/ijkplayer-armv7a-release.aar","libs/ijkplayer-java-release.aar")) + + implementation(libs.conscrypt) + implementation(libs.okhttp.logging) } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 04b0404f..73a499c6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,7 +16,8 @@ + android:screenOrientation="landscape" + android:launchMode="singleTask"> diff --git a/app/src/main/java/com/lizongying/mytv0/ChannelFragment.kt b/app/src/main/java/com/lizongying/mytv0/ChannelFragment.kt index ccdc0f3e..a3e7f848 100644 --- a/app/src/main/java/com/lizongying/mytv0/ChannelFragment.kt +++ b/app/src/main/java/com/lizongying/mytv0/ChannelFragment.kt @@ -2,6 +2,7 @@ package com.lizongying.mytv0 import android.os.Bundle import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -13,60 +14,71 @@ import com.lizongying.mytv0.models.TVModel class ChannelFragment : Fragment() { private var _binding: ChannelBinding? = null - private val binding get() = _binding!! + private val binding get() = _binding - private val handler = Handler() + private val handler = Handler(Looper.getMainLooper()) private val delay: Long = 3000 private var channel = 0 override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View { + ): View? { _binding = ChannelBinding.inflate(inflater, container, false) - _binding!!.root.visibility = View.GONE + return _binding?.root?.apply { + visibility = View.GONE + } + } - val application = requireActivity().applicationContext as MyTVApplication + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupUI() + } - binding.channel.layoutParams.width = application.px2Px(binding.channel.layoutParams.width) - binding.channel.layoutParams.height = application.px2Px(binding.channel.layoutParams.height) + private fun setupUI() { + binding?.let { binding -> + val application = requireActivity().applicationContext as MyTVApplication - 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 + binding.channel.layoutParams.width = application.px2Px(binding.channel.layoutParams.width) + binding.channel.layoutParams.height = application.px2Px(binding.channel.layoutParams.height) - binding.content.textSize = application.px2PxFont(binding.content.textSize) - binding.time.textSize = application.px2PxFont(binding.time.textSize) + 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 - binding.main.layoutParams.width = application.shouldWidthPx() - binding.main.layoutParams.height = application.shouldHeightPx() + binding.content.textSize = application.px2PxFont(binding.content.textSize) + binding.time.textSize = application.px2PxFont(binding.time.textSize) - return binding.root + binding.main.layoutParams.width = application.shouldWidthPx() + binding.main.layoutParams.height = application.shouldHeightPx() + } } fun show(tvViewModel: TVModel) { handler.removeCallbacks(hideRunnable) handler.removeCallbacks(playRunnable) - binding.content.text = (tvViewModel.tv.id.plus(1)).toString() + binding?.content?.text = (tvViewModel.tv.id.plus(1)).toString() view?.visibility = View.VISIBLE handler.postDelayed(hideRunnable, delay) } fun show(channel: String) { - if (binding.content.text.length > 1) { - return - } - this.channel = "${binding.content.text}$channel".toInt() - handler.removeCallbacks(hideRunnable) - handler.removeCallbacks(playRunnable) - if (binding.content.text == "") { - binding.content.text = channel - view?.visibility = View.VISIBLE - handler.postDelayed(playRunnable, delay) - } else { - binding.content.text = "${binding.content.text}$channel" - handler.postDelayed(playRunnable, 0) + binding?.let { binding -> + if (binding.content.text.length > 1) { + return + } + this.channel = "${binding.content.text}$channel".toInt() + handler.removeCallbacks(hideRunnable) + handler.removeCallbacks(playRunnable) + if (binding.content.text.isEmpty()) { + binding.content.text = channel + view?.visibility = View.VISIBLE + handler.postDelayed(playRunnable, delay) + } else { + binding.content.text = "${binding.content.text}$channel" + handler.postDelayed(playRunnable, 0) + } } } @@ -84,17 +96,19 @@ class ChannelFragment : Fragment() { } private val hideRunnable = Runnable { - binding.content.text = "" + binding?.content?.text = "" view?.visibility = View.GONE } private val playRunnable = Runnable { - (activity as MainActivity).play(channel - 1) + (activity as? MainActivity)?.play(channel - 1) handler.postDelayed(hideRunnable, delay) } override fun onDestroyView() { super.onDestroyView() + handler.removeCallbacks(hideRunnable) + handler.removeCallbacks(playRunnable) _binding = null } diff --git a/app/src/main/java/com/lizongying/mytv0/MainActivity.kt b/app/src/main/java/com/lizongying/mytv0/MainActivity.kt index aad2b620..d2c37efa 100644 --- a/app/src/main/java/com/lizongying/mytv0/MainActivity.kt +++ b/app/src/main/java/com/lizongying/mytv0/MainActivity.kt @@ -421,7 +421,11 @@ class MainActivity : FragmentActivity() { private val hideSetting = Runnable { if (!settingFragment.isHidden) { - supportFragmentManager.beginTransaction().hide(settingFragment).commitNow() + if (!supportFragmentManager.isDestroyed) { + supportFragmentManager.beginTransaction().hide(settingFragment).commitNow() + } else { + Log.e(TAG, "SupportFragmentManager is destroyed!") + } showTime() } } diff --git a/app/src/main/java/com/lizongying/mytv0/PlayerFragment.kt b/app/src/main/java/com/lizongying/mytv0/PlayerFragment.kt index 2fb7dedc..9f36129f 100644 --- a/app/src/main/java/com/lizongying/mytv0/PlayerFragment.kt +++ b/app/src/main/java/com/lizongying/mytv0/PlayerFragment.kt @@ -7,28 +7,14 @@ import android.view.SurfaceHolder import android.view.SurfaceView import android.view.View import android.view.ViewGroup -import android.view.ViewTreeObserver import androidx.annotation.OptIn import androidx.fragment.app.Fragment import androidx.media3.common.MimeTypes -import androidx.media3.common.PlaybackException -import androidx.media3.common.Player -import androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION -import androidx.media3.common.Player.REPEAT_MODE_ALL -import androidx.media3.common.VideoSize import androidx.media3.common.util.UnstableApi -import androidx.media3.datasource.DataSource -import androidx.media3.datasource.DataSpec -import androidx.media3.datasource.DefaultHttpDataSource -import androidx.media3.datasource.TransferListener -import androidx.media3.exoplayer.DefaultRenderersFactory -import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.mediacodec.MediaCodecSelector import androidx.media3.exoplayer.mediacodec.MediaCodecUtil import com.lizongying.mytv0.databinding.PlayerBinding -import com.lizongying.mytv0.models.SourceType import com.lizongying.mytv0.models.TVModel -import tv.danmaku.ijk.media.player.IjkMediaPlayer class PlayerFragment : Fragment(), SurfaceHolder.Callback { private var _binding: PlayerBinding? = null @@ -113,6 +99,11 @@ class PlayerFragment : Fragment(), SurfaceHolder.Callback { override fun onStart() { Log.i(TAG, "onStart") super.onStart() + } + + override fun onResume() { + Log.i(TAG, "play-onResume") + super.onResume() if (ijkUtil?.isPlaying == false) { Log.i(TAG, "replay") ijkUtil?.start() diff --git a/app/src/main/java/com/lizongying/mytv0/UpdateManager.kt b/app/src/main/java/com/lizongying/mytv0/UpdateManager.kt index 6d58e3ee..1661b453 100644 --- a/app/src/main/java/com/lizongying/mytv0/UpdateManager.kt +++ b/app/src/main/java/com/lizongying/mytv0/UpdateManager.kt @@ -1,49 +1,45 @@ package com.lizongying.mytv0 -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.fragment.app.FragmentActivity import com.lizongying.mytv0.requests.HttpClient import com.lizongying.mytv0.requests.ReleaseRequest import com.lizongying.mytv0.requests.ReleaseResponse -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File - +import java.io.IOException class UpdateManager( - private var context: Context, - private var versionCode: Long -) : - ConfirmationFragment.ConfirmationListener { + private val context: Context, + private val versionCode: Long +) : ConfirmationFragment.ConfirmationListener { private var releaseRequest = ReleaseRequest() private var release: ReleaseResponse? = null - - private var downloadReceiver: DownloadReceiver? = null + private val okHttpClient = HttpClient.okHttpClient + private var downloadJob: Job? = null + private var lastLoggedProgress = -1 fun checkAndUpdate() { Log.i(TAG, "checkAndUpdate") - CoroutineScope(Dispatchers.Main).launch { + GlobalScope.launch(Dispatchers.Main) { var text = "版本获取失败" var update = false try { release = releaseRequest.getRelease() Log.i(TAG, "versionCode $versionCode ${release?.version_code}") if (release?.version_code != null) { - if (release?.version_code!! >= versionCode) { + if (release?.version_code!! > versionCode) { text = "最新版本:${release?.version_name}" update = true } else { @@ -65,171 +61,114 @@ class UpdateManager( private fun startDownload(release: ReleaseResponse) { val apkName = "my-tv-0" val apkFileName = "$apkName-${release.version_name}.apk" - val downloadManager = - context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - val request = - Request(Uri.parse("${HttpClient.DOWNLOAD_HOST}${release.version_name}/$apkName-${release.version_name}.apk")) - Log.i( - TAG, - "url ${Uri.parse("${HttpClient.DOWNLOAD_HOST}${release.version_name}/$apkName-${release.version_name}.apk")}" - ) - context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.mkdirs() - Log.i(TAG, "save dir ${Environment.DIRECTORY_DOWNLOADS}") - request.setDestinationInExternalFilesDir( - context, - Environment.DIRECTORY_DOWNLOADS, - apkFileName - ) - request.setTitle("${context.resources.getString(R.string.app_name)} ${release.version_name}") - 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) - ) + val url = + "${HttpClient.DOWNLOAD_HOST}${release.version_name}/$apkName-${release.version_name}.apk" + var downloadDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) + if (downloadDir == null) { + downloadDir = File(context.filesDir, "downloads") } - getDownloadProgress(context, downloadReference) { progress -> - println("Download progress: $progress%") + cleanupDownloadDirectory(downloadDir, apkName) + val file = File(downloadDir, apkFileName) + file.parentFile?.mkdirs() + + downloadJob = GlobalScope.launch(Dispatchers.IO) { + downloadWithRetry(url, file) } } - 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 - } - } - } + private fun cleanupDownloadDirectory(directory: File?, apkNamePrefix: String) { + directory?.let { dir -> + dir.listFiles()?.forEach { file -> + if (file.name.startsWith(apkNamePrefix) && file.name.endsWith(".apk")) { + val deleted = file.delete() + if (deleted) { + Log.i(TAG, "Deleted old APK file: ${file.name}") + } else { + Log.e(TAG, "Failed to delete old APK file: ${file.name}") } } - -// 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") - - if (reference == downloadReference) { - val downloadManager = - context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - val query = DownloadManager.Query().setFilterById(downloadReference) - val cursor = downloadManager.query(query) - if (cursor != null && cursor.moveToFirst()) { - val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS) - if (statusIndex < 0) { - Log.i(TAG, "Download failure") - return - } - val status = cursor.getInt(statusIndex) - - val progressIndex = - cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR) - if (progressIndex < 0) { - Log.i(TAG, "Download failure") - return + private suspend fun downloadWithRetry(url: String, file: File, maxRetries: Int = 3) { + var retries = 0 + while (retries < maxRetries) { + try { + downloadFile(url, file) + // If download is successful, break the loop + break + } catch (e: IOException) { + Log.e(TAG, "Download failed: ${e.message}") + retries++ + if (retries >= maxRetries) { + Log.e(TAG, "Max retries reached. Download failed.") + withContext(Dispatchers.Main) { + // Notify user about download failure + updateUI("下载失败,请检查网络连接后重试", false) } - val progress = cursor.getInt(progressIndex) - - val totalSizeIndex = - cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES) - val totalSize = cursor.getInt(totalSizeIndex) - - cursor.close() - - when (status) { - DownloadManager.STATUS_SUCCESSFUL -> { - installNewVersion() - } - - DownloadManager.STATUS_FAILED -> { - // Handle download failure - Log.i(TAG, "Download failure") - } + } else { + Log.i(TAG, "Retrying download (${retries}/${maxRetries})") + delay(30000) // Wait for 30 seconds before retrying + } + } + } + } - else -> { - // Update UI with download progress - val percentage = progress * 100 / totalSize - Log.i(TAG, "Download progress: $percentage%") - } + private suspend fun downloadFile(url: String, file: File) { + val request = okhttp3.Request.Builder().url(url).build() + val response = okHttpClient.newCall(request).execute() + if (!response.isSuccessful) throw IOException("Unexpected code $response") + + val body = response.body() ?: throw IOException("Null response body") + val contentLength = body.contentLength() + var bytesRead = 0L + + body.byteStream().use { inputStream -> + file.outputStream().use { outputStream -> + val buffer = ByteArray(BUFFER_SIZE) + var bytes: Int + while (inputStream.read(buffer).also { bytes = it } != -1) { + outputStream.write(buffer, 0, bytes) + bytesRead += bytes + val progress = + if (contentLength > 0) (bytesRead * 100 / contentLength).toInt() else -1 + withContext(Dispatchers.Main) { + updateDownloadProgress(progress) } } } } - private fun installNewVersion() { - val apkFile = File( - context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), - apkFileName - ) - Log.i(TAG, "apkFile $apkFile") - - if (apkFile.exists()) { - 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) - } + withContext(Dispatchers.Main) { + installNewVersion(file) + } + } - context.startActivity(installIntent) - } else { - Log.e(TAG, "APK file does not exist!") - } + private fun updateDownloadProgress(progress: Int) { + if (progress == -1) { + // Log when progress can't be determined + Log.i(TAG, "Download in progress, size unknown") + } else if (progress % 10 == 0 && progress != lastLoggedProgress) { + // Log every 10% and avoid duplicate logs + Log.i(TAG, "Download progress: $progress%") + lastLoggedProgress = progress + "升级文件已经下载:${progress}%".showToast() } } - companion object { - private const val TAG = "UpdateManager" + private fun installNewVersion(apkFile: File) { + if (apkFile.exists()) { + val apkUri = Uri.fromFile(apkFile) // Use Uri.fromFile for Android 4.4 + Log.i(TAG, "apkUri $apkUri") + val installIntent = Intent(Intent.ACTION_VIEW).apply { + setDataAndType(apkUri, "application/vnd.android.package-archive") + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(installIntent) + } else { + Log.e(TAG, "APK file does not exist!") + } } override fun onConfirm() { @@ -238,12 +177,16 @@ class UpdateManager( } override fun onCancel() { + // Handle cancellation if needed } fun destroy() { - if (downloadReceiver != null) { - context.unregisterReceiver(downloadReceiver) - Log.i(TAG, "destroy downloadReceiver") - } + downloadJob?.cancel() + // Additional cleanup if needed + } + + companion object { + private const val TAG = "UpdateManager" + private const val BUFFER_SIZE = 8192 } } \ No newline at end of file diff --git a/app/src/main/java/com/lizongying/mytv0/models/TVList.kt b/app/src/main/java/com/lizongying/mytv0/models/TVList.kt index df90ea40..d57dc624 100644 --- a/app/src/main/java/com/lizongying/mytv0/models/TVList.kt +++ b/app/src/main/java/com/lizongying/mytv0/models/TVList.kt @@ -67,9 +67,10 @@ object TVList { update(it) } } else if (!epg.isNullOrEmpty()) { - CoroutineScope(Dispatchers.IO).launch { - updateEPG() - } +// //not enable at this version +// CoroutineScope(Dispatchers.IO).launch { +// updateEPG() +// } } } diff --git a/app/src/main/java/com/lizongying/mytv0/requests/HttpClient.kt b/app/src/main/java/com/lizongying/mytv0/requests/HttpClient.kt index 8f0e4ad8..fce0900c 100644 --- a/app/src/main/java/com/lizongying/mytv0/requests/HttpClient.kt +++ b/app/src/main/java/com/lizongying/mytv0/requests/HttpClient.kt @@ -1,27 +1,22 @@ package com.lizongying.mytv0.requests - -import android.net.Uri -import android.os.Build import android.util.Log -import com.lizongying.mytv0.SP import okhttp3.ConnectionSpec +import okhttp3.Interceptor import okhttp3.OkHttpClient -import okhttp3.TlsVersion +import okhttp3.logging.HttpLoggingInterceptor +import org.conscrypt.Conscrypt import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -import java.net.InetSocketAddress -import java.net.Proxy -import java.security.KeyStore +import java.security.Security +import java.util.Collections +import java.util.concurrent.TimeUnit import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager -import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager - object HttpClient { const val TAG = "HttpClient" - private const val HOST = "https://ghproxy.org/https://github.com/vrichv/my-tv-0/raw/" + private const val HOST = "https://ghproxy.org/https://raw.githubusercontent.com/vrichv/my-tv-0/" const val DOWNLOAD_HOST = "https://ghproxy.org/https://github.com/vrichv/my-tv-0/releases/download/" val okHttpClient: OkHttpClient by lazy { @@ -42,89 +37,49 @@ object HttpClient { .build().create(ConfigService::class.java) } - private fun enableTls12OnPreLollipop(client: OkHttpClient.Builder): OkHttpClient.Builder { - if (Build.VERSION.SDK_INT < 22) { - try { - val sc = SSLContext.getInstance("TLSv1.2") - - sc.init(null, null, null) - - // a more robust version is to pass a custom X509TrustManager - // as the second parameter and make checkServerTrusted to accept your server. - // Credits: https://github.com/square/okhttp/issues/2372#issuecomment-1774955225 - val trustManagerFactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm() + private fun getUnsafeOkHttpClient(): OkHttpClient { + // Init Conscrypt + val conscrypt = Conscrypt.newProvider() + // Add as provider + Security.insertProviderAt(conscrypt, 1) + // OkHttp 3.12.x + // ConnectionSpec.COMPATIBLE_TLS = TLS1.0 + // ConnectionSpec.MODERN_TLS = TLS1.0 + TLS1.1 + TLS1.2 + TLS 1.3 + // ConnectionSpec.RESTRICTED_TLS = TLS 1.2 + TLS 1.3 + val okHttpBuilder = OkHttpClient.Builder() + .connectionSpecs(Collections.singletonList(ConnectionSpec.MODERN_TLS)) + + val userAgentInterceptor = Interceptor { chain -> + val originalRequest = chain.request() + val requestWithUserAgent = originalRequest.newBuilder() + .header( + "User-Agent", + "Mozilla/5.0 (Linux; Android 4.4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Mobile Safari/537.36" ) - trustManagerFactory.init(null as KeyStore?) - val trustManagers = trustManagerFactory.trustManagers - check(!(trustManagers.size != 1 || trustManagers[0] !is X509TrustManager)) { - ("Unexpected default trust managers:" - + trustManagers.contentToString()) - } - val trustManager = trustManagers[0] as X509TrustManager - - client.sslSocketFactory(Tls12SocketFactory(sc.socketFactory), trustManager) - - val cs = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) - .tlsVersions(TlsVersion.TLS_1_2) - .build() - - val specs: MutableList = ArrayList() - specs.add(cs) - specs.add(ConnectionSpec.COMPATIBLE_TLS) - specs.add(ConnectionSpec.CLEARTEXT) - - client.connectionSpecs(specs) - } catch (exc: java.lang.Exception) { - Log.e("OkHttpTLSCompat", "Error while setting TLS 1.2", exc) - } + .build() + chain.proceed(requestWithUserAgent) } - return client - } + val loggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BASIC + } - private fun getUnsafeOkHttpClient(): OkHttpClient { try { - val trustAllCerts: Array = arrayOf( - object : X509TrustManager { - override fun checkClientTrusted( - chain: Array?, - authType: String? - ) { - } - - override fun checkServerTrusted( - chain: Array?, - authType: String? - ) { - } - - override fun getAcceptedIssuers(): Array { - return emptyArray() - } - } - ) - - val sslContext = SSLContext.getInstance("SSL") - sslContext.init(null, trustAllCerts, java.security.SecureRandom()) - - val builder = OkHttpClient.Builder() - .sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager) - .hostnameVerifier { _, _ -> true } - .connectionSpecs(listOf(ConnectionSpec.COMPATIBLE_TLS, ConnectionSpec.CLEARTEXT)) - .dns(DnsCache()) - - if (SP.proxy != "") { - Log.i(TAG, "proxy ${SP.proxy}") - val uri = Uri.parse(SP.proxy) - val proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress(uri.host, uri.port)) - builder.proxy(proxy) - } - - return enableTls12OnPreLollipop(builder).build() + //FIXME: NOT-SAFE + //val tm: X509TrustManager = Conscrypt.getDefaultX509TrustManager() + val tm: X509TrustManager = InternalX509TrustManager() + val sslContext = SSLContext.getInstance("TLS", conscrypt) + sslContext.init(null, arrayOf(tm), null) + okHttpBuilder.sslSocketFactory(InternalSSLSocketFactory(sslContext.socketFactory), tm) } catch (e: Exception) { - throw RuntimeException(e) + Log.e(TAG, "Error setting up OkHttpClient", e) } + + return okHttpBuilder.dns(DnsCache()).retryOnConnectionFailure(true) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS).addInterceptor(userAgentInterceptor) + .addInterceptor(loggingInterceptor).build() } } \ No newline at end of file diff --git a/app/src/main/java/com/lizongying/mytv0/requests/InternalSSLSocketFactory.kt b/app/src/main/java/com/lizongying/mytv0/requests/InternalSSLSocketFactory.kt new file mode 100644 index 00000000..c7a6fe14 --- /dev/null +++ b/app/src/main/java/com/lizongying/mytv0/requests/InternalSSLSocketFactory.kt @@ -0,0 +1,55 @@ +package com.lizongying.mytv0.requests + +import java.io.IOException +import java.net.InetAddress +import java.net.Socket +import javax.net.ssl.SSLSocket +import javax.net.ssl.SSLSocketFactory + +class InternalSSLSocketFactory(private val mSSLSocketFactory: SSLSocketFactory) : SSLSocketFactory() { + + override fun getDefaultCipherSuites(): Array { + return mSSLSocketFactory.defaultCipherSuites + } + + override fun getSupportedCipherSuites(): Array { + return mSSLSocketFactory.supportedCipherSuites + } + + @Throws(IOException::class) + override fun createSocket(): Socket { + return enableTLSOnSocket(mSSLSocketFactory.createSocket()) + } + + @Throws(IOException::class) + override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket { + return enableTLSOnSocket(mSSLSocketFactory.createSocket(s, host, port, autoClose)) + } + + @Throws(IOException::class) + override fun createSocket(host: String, port: Int): Socket { + return enableTLSOnSocket(mSSLSocketFactory.createSocket(host, port)) + } + + @Throws(IOException::class) + override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket { + return enableTLSOnSocket(mSSLSocketFactory.createSocket(host, port, localHost, localPort)) + } + + @Throws(IOException::class) + override fun createSocket(host: InetAddress, port: Int): Socket { + return enableTLSOnSocket(mSSLSocketFactory.createSocket(host, port)) + } + + @Throws(IOException::class) + override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket { + return enableTLSOnSocket(mSSLSocketFactory.createSocket(address, port, localAddress, localPort)) + } + + private fun enableTLSOnSocket(socket: Socket): Socket { + if (socket is SSLSocket) { + socket.enabledProtocols = arrayOf("TLSv1.2", "TLSv1.3") + } + return socket + } +} diff --git a/app/src/main/java/com/lizongying/mytv0/requests/InternalX509TrustManager.kt b/app/src/main/java/com/lizongying/mytv0/requests/InternalX509TrustManager.kt new file mode 100644 index 00000000..ad7f63dc --- /dev/null +++ b/app/src/main/java/com/lizongying/mytv0/requests/InternalX509TrustManager.kt @@ -0,0 +1,32 @@ +package com.lizongying.mytv0.requests + +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import javax.net.ssl.X509TrustManager + +class InternalX509TrustManager : X509TrustManager { + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String) { + try { + chain[0].checkValidity() + } catch (e: Exception) { + e.printStackTrace() + throw CertificateException("Certificate not valid or trusted.") + } + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, authType: String) { + try { + chain[0].checkValidity() + } catch (e: Exception) { + e.printStackTrace() + throw CertificateException("Certificate not valid or trusted.") + } + } + + override fun getAcceptedIssuers(): Array { + return arrayOf() + } +} diff --git a/app/src/main/java/com/lizongying/mytv0/requests/Tls12SocketFactory.kt b/app/src/main/java/com/lizongying/mytv0/requests/Tls12SocketFactory.kt deleted file mode 100644 index 38638010..00000000 --- a/app/src/main/java/com/lizongying/mytv0/requests/Tls12SocketFactory.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.lizongying.mytv0.requests - -import java.io.IOException -import java.net.InetAddress -import java.net.Socket -import java.net.UnknownHostException -import javax.net.ssl.SSLSocket -import javax.net.ssl.SSLSocketFactory - - -/** - * Enables TLS v1.2 when creating SSLSockets. - * - * - * For some reason, android supports TLS v1.2 from API 16, but enables it by - * default only from API 20. - * @link https://developer.android.com/reference/javax/net/ssl/SSLSocket.html - * @see SSLSocketFactory - */ -class Tls12SocketFactory(val delegate: SSLSocketFactory) : SSLSocketFactory() { - override fun getDefaultCipherSuites(): Array { - return delegate.defaultCipherSuites - } - - override fun getSupportedCipherSuites(): Array { - return delegate.supportedCipherSuites - } - - @Throws(IOException::class) - override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket { - return patch(delegate.createSocket(s, host, port, autoClose)) - } - - @Throws(IOException::class, UnknownHostException::class) - override fun createSocket(host: String, port: Int): Socket { - return patch(delegate.createSocket(host, port)) - } - - @Throws(IOException::class, UnknownHostException::class) - override fun createSocket( - host: String, - port: Int, - localHost: InetAddress, - localPort: Int - ): Socket { - return patch(delegate.createSocket(host, port, localHost, localPort)) - } - - @Throws(IOException::class) - override fun createSocket(host: InetAddress, port: Int): Socket { - return patch(delegate.createSocket(host, port)) - } - - @Throws(IOException::class) - override fun createSocket( - address: InetAddress, - port: Int, - localAddress: InetAddress, - localPort: Int - ): Socket { - return patch(delegate.createSocket(address, port, localAddress, localPort)) - } - - private fun patch(s: Socket): Socket { - if (s is SSLSocket) { - s.enabledProtocols = TLS_V12_ONLY - } - return s - } - - companion object { - private val TLS_V12_ONLY = arrayOf("TLSv1.2") - } -} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8d0c62aa..6d8ed9f6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - 我的電視·〇 + 我的電視〇IJK 换台反转 换台时显示频道号 更新应用 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fb9063ed..fe513d7b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,12 +7,13 @@ recyclerview = "1.3.2" zxing = "3.5.3" glide = "4.16.0" # java7 -gson = "2.10.1" # 19:2.10.1 -okhttp = "3.12.13" +gson = "2.10.1" # 19:2.10.1 #api17:2.9.1 +okhttp = "3.12.13" # 19: 3.12.13 retrofit = "2.6.4" # 21:2.9.0 17:2.6.4 +conscrypt = "2.5.2" work = "2.9.0" -core_ktx = "1.13.1" +core_ktx = "1.13.1" #api17:1.12.0 multidex = "2.0.1" leanback = "1.0.0" lifecycle = "2.8.3" @@ -42,6 +43,8 @@ gson = { module = "com.google.code.gson:gson", version.ref = "gson" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } +conscrypt = { module = "org.conscrypt:conscrypt-android", version.ref = "conscrypt" } +okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } core-ktx = { module = "androidx.core:core-ktx", version.ref = "core_ktx" } multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" } diff --git a/version.json b/version.json index f5f8e22f..bad68d46 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version_code": 16910338, "version_name": "v1.2.8-kk2"} +{"version_code": 16910341, "version_name": "v1.2.8-ijk5"}