├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── libs │ ├── lib-decoder-ffmpeg-release.aar │ └── lib-decoder-vp9-release.aar ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── ww │ │ └── simpletv │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── ww │ │ │ └── simpletv │ │ │ ├── AppUtils.kt │ │ │ ├── BaseActivity.kt │ │ │ ├── ChannelUtils.kt │ │ │ ├── Constant.kt │ │ │ ├── DownloadStatus.kt │ │ │ ├── FontSizeActivity.kt │ │ │ ├── MainActivity.kt │ │ │ ├── PlayerActivity.kt │ │ │ ├── RetryInterceptor.kt │ │ │ ├── TVApplication.kt │ │ │ ├── TVBootReceive.kt │ │ │ ├── adapter │ │ │ ├── ChannelAdapter.kt │ │ │ ├── FontSizeAdapter.kt │ │ │ └── GroupAdapter.kt │ │ │ ├── bean │ │ │ ├── TV.kt │ │ │ └── VersionInfo.kt │ │ │ └── dialog │ │ │ ├── BaseDialogFragment.kt │ │ │ ├── ChannelListDialog.kt │ │ │ ├── SettingDialog.kt │ │ │ └── UpdateDialog.kt │ └── res │ │ ├── drawable │ │ ├── arrow.xml │ │ ├── btn_selector.xml │ │ ├── focus_bg.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_logo.xml │ │ ├── item_focus_selector.xml │ │ ├── place_icon.xml │ │ ├── simple_tv_icon.xml │ │ ├── switch_selector.xml │ │ ├── switch_thumb_selector.xml │ │ └── switch_track_selector.xml │ │ ├── layout │ │ ├── activity_font_size.xml │ │ ├── activity_main.xml │ │ ├── activity_player.xml │ │ ├── dialog_channel.xml │ │ ├── dialog_setting.xml │ │ ├── dialog_update.xml │ │ ├── item_channel.xml │ │ ├── item_font_size.xml │ │ └── item_group.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── bg_window.png │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── bg_window.png │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── bg_window.png │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── bg_window.png │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── bg_window.png │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── bg_window.png │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── themes.xml │ │ ├── values │ │ ├── arrays.xml │ │ ├── colors.xml │ │ ├── dimen.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── provider.xml │ └── test │ └── java │ └── com │ └── ww │ └── simpletv │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── a1.png ├── control.jpg ├── q1.png ├── q2.png └── telegram.png ├── m3u └── ipv6 │ └── IPTV.m3u └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | replay_pid* 25 | *.iml 26 | .gradle 27 | /local.properties 28 | .idea 29 | .DS_Store 30 | /build 31 | /captures 32 | .externalNativeBuild 33 | .cxx 34 | local.properties -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | **1.出现“初始化频道列表失败,请尝试手动更新或退出重试”、“播放失败,网络连接超时,请稍后重试”** 4 | 5 | ![error1](img/q1.png) 6 | ![error2](img/q2.png) 7 | 8 | 1.请尝试手动更新频道源,更新成功之后退出重新进入,如果更新失败请确认网络是否连通 9 | ![手动更新](img/a1.png) 10 | 11 | 2.如果更新成功后还是无法播放,可能该频道源已失效,请切换频道观看,耐心等待更新修复 12 | 3.请确认网络是否能访问IPV6,测试地址测试[地址1](http://test-ipv6.com/),[地址2](http://ipv6-test.ch/index.html.zh_CN)。如果已经路由器开启IPV6但是无法访问IPV6,请重启路由器,建议开启路由器每日定时重启功能,防止IPV6失效。 13 | 14 | # SimpleTV 15 | 16 | 1.观看电视直播,支持央视和地方台。 操作简单,打开即看,方便中老年人使用,让电视回归本质。 17 | 2.部分频道目前仅支持IPV6网络,IPV4网络无法播放。 18 | 3.IPV6测试[地址1](http://test-ipv6.com/),[地址2](http://ipv6-test.ch/index.html.zh_CN),如显示运营商已接入IPV6但无法访问IPV6网站,需要在路由器设置打开IPV6功能([华为路由器参考](https://consumer.huawei.com/cn/support/content/zh-cn00685838/#:~:text=%E9%80%9A%E8%BF%87%E6%99%BA%E6%85%A7%E7%94%9F%E6%B4%BB%20App%20%E8%AE%BE%E7%BD%AE%201%20%E6%89%8B%E6%9C%BA%2F%E5%B9%B3%E6%9D%BF%E8%BF%9E%E6%8E%A5%E5%88%B0%E8%B7%AF%E7%94%B1%E5%99%A8%E7%9A%84%20Wi-Fi%E3%80%82%202%20%E6%89%93%E5%BC%80%E6%99%BA%E6%85%A7%E7%94%9F%E6%B4%BB,%E7%82%B9%E5%87%BB%20IPv6%20%E3%80%82%20%E7%82%B9%E5%87%BB%20IPv6%20%E5%BC%80%E5%85%B3%EF%BC%8C%E5%BC%80%E5%90%AF%E6%88%96%E5%85%B3%E9%97%AD%20IPv6%20%E5%8A%9F%E8%83%BD%E3%80%82),[小米路由器参考](https://cdn.cnbj1.fds.api.mi-img.com/ics-resources/articles/6055c933ec317cb4ee2d0103.html)) 19 | 4.仅支持安卓6.0及以上版本 20 | 5.所有素材及直播源均来源于互联网,**仅供测试研究,不得商用**。如有侵权,请联系我删除。 21 | 22 | # 安装 23 | 24 | 1.下载apk后使用U盘安装(TCL电视系统屏蔽了apk文件,需要用自带的电视卫视进入U盘安装) 25 | 2.使用当贝市场远程推送安装([参考](https://zhuanlan.zhihu.com/p/588748827#:~:text=%E6%95%99%E7%A8%8B%E4%BB%8B%E7%BB%8D%20%E6%AD%A5%E9%AA%A4%E4%B8%80%EF%BC%9A%20%E9%A6%96%E5%85%88%E6%89%93%E5%BC%80%E5%BD%93%E8%B4%9D%E5%B8%82%E5%9C%BA%E7%9A%84%E7%AE%A1%E7%90%86%E7%95%8C%E9%9D%A2%EF%BC%8C%20%E5%A6%82%E5%9B%BE%E6%89%80%E7%A4%BA%20%E7%9A%84%E4%BD%8D%E7%BD%AE%E5%8F%AF%E4%BB%A5%E7%9C%8B%E5%88%B0%20%E2%80%9C%E8%BF%9C%E7%A8%8B%E6%8E%A8%E9%80%81%E2%80%9D%E7%9A%84%E5%9B%BE%E6%A0%87%20%E6%AD%A5%E9%AA%A4%E4%BA%8C%EF%BC%9A,%E6%9C%89%20%E4%B8%A4%E7%A7%8D%E8%BF%9E%E6%8E%A5%E6%96%B9%E5%BC%8F%20%EF%BC%8C%E8%BF%9C%E7%A8%8B%E6%8E%A8%E9%80%81%E5%BC%80%E5%A7%8B%E5%89%8D%E9%9C%80%E8%A6%81%E5%9C%A8%20%E6%89%8B%E6%9C%BA%E4%B8%8A%E6%8F%90%E5%89%8D%E4%B8%8B%E8%BD%BD%E5%A5%BD%E9%9C%80%E8%A6%81%E6%8E%A8%E9%80%81%E7%9A%84%E8%BD%AF%E4%BB%B6%20%E6%AD%A5%E9%AA%A4%E4%B8%89%EF%BC%9A%20%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95%E4%B8%80%E6%89%8B%E6%9C%BA%E6%89%AB%E7%A0%81%E5%90%8E%E4%BC%9A%20%E8%87%AA%E5%8A%A8%E8%B7%B3%E8%BD%AC%E5%88%B0%E8%BF%9C%E7%A8%8B%E6%8E%A8%E9%80%81%E7%9A%84%E9%A1%B5%E9%9D%A2%20%EF%BC%8C%E7%82%B9%E5%87%BB%E4%B8%8A%E4%BC%A0%E6%96%87%E4%BB%B6%EF%BC%8C%E5%B0%86%E4%B8%8B%E8%BD%BD%E5%A5%BD%E7%9A%84%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0)) 26 | 3.打开电视开发者模式,连接电脑使用adb安装 27 | 28 | # 操作说明 29 | 30 | 小米遥控器示例 31 | ![小米遥控器](img/control.jpg) 32 | 33 | # 频道展示 34 | 35 | 小米盒子4C 36 | ![CCTV1 综合](img/1.png) 37 | ![湖南卫视](img/2.png) 38 | ![CHC高清电影](/img/3.png) 39 | ![湖北经视](img/4.png) 40 | ![NewTV军事评论](img/5.png) 41 | 42 | # 致谢 43 | 44 | 直播来源: 45 | https://github.com/Meroser/IPTV 46 | https://github.com/fanmingming/live 47 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | .gradle 3 | /release -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | id("kotlin-kapt") 5 | } 6 | 7 | android { 8 | namespace = "com.ww.simpletv" 9 | compileSdk = 34 10 | 11 | defaultConfig { 12 | applicationId = "com.ww.simpletv" 13 | minSdk = 23 14 | targetSdk = 34 15 | versionCode = 4 16 | versionName = "1.0.3" 17 | 18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 19 | } 20 | 21 | buildTypes { 22 | release { 23 | isMinifyEnabled = false 24 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 25 | } 26 | } 27 | compileOptions { 28 | sourceCompatibility = JavaVersion.VERSION_1_8 29 | targetCompatibility = JavaVersion.VERSION_1_8 30 | } 31 | kotlinOptions { 32 | jvmTarget = "1.8" 33 | } 34 | buildFeatures { 35 | dataBinding = true 36 | } 37 | } 38 | 39 | dependencies { 40 | 41 | implementation("androidx.core:core-ktx:1.12.0") 42 | implementation("androidx.appcompat:appcompat:1.6.1") 43 | implementation("androidx.leanback:leanback:1.0.0") 44 | implementation("com.google.android.material:material:1.11.0") 45 | implementation("androidx.constraintlayout:constraintlayout:2.1.4") 46 | implementation("com.github.bumptech.glide:glide:4.16.0") 47 | implementation("com.tencent:mmkv:1.3.3") 48 | implementation("com.google.code.gson:gson:2.10") 49 | testImplementation("junit:junit:4.13.2") 50 | androidTestImplementation("androidx.test.ext:junit:1.1.5") 51 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") 52 | implementation("androidx.media3:media3-exoplayer:1.2.1") 53 | implementation("androidx.media3:media3-exoplayer-hls:1.2.1") 54 | implementation("androidx.media3:media3-ui:1.2.1") 55 | implementation("com.squareup.okhttp3:okhttp:4.12.0") 56 | implementation(files("libs/lib-decoder-ffmpeg-release.aar")) 57 | implementation(files("libs/lib-decoder-vp9-release.aar")) 58 | } 59 | -------------------------------------------------------------------------------- /app/libs/lib-decoder-ffmpeg-release.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Potato-66/SimpleTV/c5c6064b61ff0636385bde50f6fd58a5f78be195/app/libs/lib-decoder-ffmpeg-release.aar -------------------------------------------------------------------------------- /app/libs/lib-decoder-vp9-release.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Potato-66/SimpleTV/c5c6064b61ff0636385bde50f6fd58a5f78be195/app/libs/lib-decoder-vp9-release.aar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -dontwarn com.tencent.bugly.** 23 | -keep public class com.tencent.bugly.**{*;} -------------------------------------------------------------------------------- /app/src/androidTest/java/com/ww/simpletv/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.ww.simpletv", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 16 | 17 | 29 | 32 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 51 | 52 | 58 | 59 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 73 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/AppUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.os.Build 8 | import android.util.Log 9 | import androidx.core.content.FileProvider 10 | import com.google.gson.Gson 11 | import com.ww.simpletv.bean.VersionInfo 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.delay 14 | import kotlinx.coroutines.flow.Flow 15 | import kotlinx.coroutines.flow.catch 16 | import kotlinx.coroutines.flow.flow 17 | import kotlinx.coroutines.flow.flowOn 18 | import kotlinx.coroutines.flow.onCompletion 19 | import okhttp3.OkHttpClient 20 | import okhttp3.Request 21 | import java.io.File 22 | import java.io.FileInputStream 23 | import java.io.FileOutputStream 24 | import java.io.InputStream 25 | import java.security.MessageDigest 26 | import java.security.NoSuchAlgorithmException 27 | 28 | /** 29 | * 30 | * Copyright (C), 2024 Potato-66, All rights reserved. 31 | * 创建时间: 2024/3/18 32 | * @since 1.0 33 | * @version 1.0 34 | * @author Potato-66 35 | */ 36 | object AppUtils { 37 | private const val TAG = "AppUtils" 38 | 39 | fun getAppVersionName(context: Context): String { 40 | return try { 41 | context.packageManager.getPackageInfo(context.packageName, 0).versionName 42 | } catch (e: Exception) { 43 | Log.e(TAG, "getAppVersion: error:${e.message}") 44 | "" 45 | } 46 | } 47 | 48 | fun getAppVersionCode(context: Context): Long { 49 | return try { 50 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 51 | context.packageManager.getPackageInfo(context.packageName, 0).longVersionCode 52 | } else { 53 | context.packageManager.getPackageInfo(context.packageName, 0).versionCode.toLong() 54 | } 55 | } catch (e: Exception) { 56 | Log.e(TAG, "getAppVersionCode: error:${e.message}") 57 | 0 58 | } 59 | } 60 | 61 | fun getVersion(): VersionInfo? { 62 | try { 63 | val build = OkHttpClient.Builder().build() 64 | build.newCall(Request.Builder().url(API.URL_VERSION).build()).execute().use { 65 | if (it.isSuccessful) { 66 | it.body?.string()?.let { json -> 67 | return Gson().fromJson(json, VersionInfo::class.java) as VersionInfo 68 | } 69 | } 70 | } 71 | } catch (e: Exception) { 72 | Log.e(TAG, "checkVersion error:${e.message}") 73 | } 74 | return null 75 | } 76 | 77 | fun download(dir: File, versionInfo: VersionInfo?): Flow { 78 | var inputStream: InputStream? = null 79 | var outputStream: FileOutputStream? = null 80 | return flow { 81 | val client = OkHttpClient.Builder().build() 82 | val url = versionInfo?.url ?: "" 83 | client.newCall(Request.Builder().url(url).build()).execute().use { response -> 84 | if (response.isSuccessful) { 85 | response.body?.let { body -> 86 | val totalLength = body.contentLength() 87 | inputStream = body.byteStream() 88 | val file = File(dir, Constant.APK_NAME) 89 | if (file.exists()) { 90 | file.delete() 91 | } 92 | outputStream = FileOutputStream(file) 93 | val buffer = ByteArray(8 * 1024) 94 | var len: Int 95 | var curLength = 0 96 | var progress = 0 97 | while ((inputStream!!.read(buffer).also { len = it } != -1)) { 98 | outputStream!!.write(buffer, 0, len) 99 | curLength += len 100 | val curProgress = (curLength * 100 / totalLength).toInt() 101 | if (curProgress - progress >= 1) { 102 | progress = curProgress 103 | emit(DownloadStatus.DownLoading(progress)) 104 | } 105 | } 106 | outputStream!!.flush() 107 | } 108 | } else { 109 | Log.e(TAG, "请求失败,error code:${response.code}") 110 | emit(DownloadStatus.Fail(response.code)) 111 | } 112 | } 113 | }.onCompletion { 114 | Log.e(TAG, "download: onCompletion") 115 | inputStream?.close() 116 | outputStream?.close() 117 | delay(1000) 118 | val file = File(dir, Constant.APK_NAME) 119 | if (file.exists()) { 120 | val md5 = getMD5(file) 121 | if (md5 != versionInfo?.md5) { 122 | Log.e(TAG, "download: md5:$md5,remote md5:${versionInfo?.md5}") 123 | emit(DownloadStatus.Fail(-1)) 124 | } else { 125 | emit(DownloadStatus.Success) 126 | } 127 | } 128 | }.catch { error -> 129 | Log.e(TAG, "download: catch error:${error.message}") 130 | inputStream?.close() 131 | outputStream?.close() 132 | emit(DownloadStatus.Error(error)) 133 | }.flowOn(Dispatchers.IO) 134 | } 135 | 136 | fun installApk(context: Context) { 137 | val intent = Intent(Intent.ACTION_VIEW) 138 | val file = File(context.externalCacheDir, Constant.APK_NAME) 139 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK 140 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 141 | intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION 142 | intent.setDataAndType( 143 | FileProvider.getUriForFile(context, "com.ww.simpletv.provider", file), 144 | "application/vnd.android.package-archive" 145 | ) 146 | } else { 147 | intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive") 148 | } 149 | context.startActivity(intent) 150 | } 151 | 152 | @OptIn(ExperimentalStdlibApi::class) 153 | fun getMD5(file: File): String { 154 | var inputStream: FileInputStream? = null 155 | return try { 156 | MessageDigest.getInstance("MD5").run { 157 | reset() 158 | inputStream = FileInputStream(file) 159 | val buffer = ByteArray(8 * 1024) 160 | var len: Int 161 | while ((inputStream!!.read(buffer).also { len = it }) != -1) { 162 | update(buffer, 0, len) 163 | } 164 | digest().toHexString() 165 | } 166 | } catch (e: NoSuchAlgorithmException) { 167 | Log.e(TAG, "getMD5: error:${e.message}") 168 | "" 169 | } catch (e: IllegalArgumentException) { 170 | Log.e(TAG, "getMD5: error:${e.message}") 171 | "" 172 | } finally { 173 | inputStream?.close() 174 | } 175 | } 176 | 177 | fun setFontScale(context: Context?, fontScale: Float) { 178 | val resources = context?.resources 179 | resources?.let { 180 | val configuration = it.configuration 181 | configuration.fontScale = fontScale 182 | context.createConfigurationContext(configuration) 183 | } 184 | } 185 | 186 | fun recreateActivity(activity: Activity) { 187 | activity.recreate() 188 | } 189 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv 2 | 3 | import android.content.Context 4 | import androidx.appcompat.app.AppCompatActivity 5 | import com.tencent.mmkv.MMKV 6 | 7 | open class BaseActivity : AppCompatActivity() { 8 | override fun attachBaseContext(newBase: Context?) { 9 | AppUtils.setFontScale(newBase, MMKV.defaultMMKV().decodeFloat(Constant.KEY_FONT_SIZE, Constant.FONT_SIZE_NORMAL)) 10 | super.attachBaseContext(newBase) 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/ChannelUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import com.tencent.mmkv.MMKV 6 | import com.ww.simpletv.bean.TV 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.withContext 9 | import okhttp3.OkHttpClient 10 | import okhttp3.Request 11 | import java.io.File 12 | 13 | /** 14 | * 15 | * Copyright (C), 2024 Potato-66, All rights reserved. 16 | * 创建时间: 2024/2/26 17 | * @since 1.0 18 | * @version 1.0 19 | * @author Potato-66 20 | */ 21 | object ChannelUtils { 22 | private const val TAG = "ChannelUtils" 23 | val channelSet = linkedSetOf() 24 | 25 | suspend fun updateChannel(context: Context): Boolean { 26 | channelSet.clear() 27 | parseChannel(context) 28 | return channelSet.isNotEmpty() 29 | } 30 | 31 | suspend fun parseChannel(context: Context) { 32 | return withContext(Dispatchers.IO) { 33 | val file = getChannelFile(context) 34 | if (file == null) { 35 | Log.e(TAG, "parseChannel: iptv file not exist") 36 | return@withContext 37 | } 38 | val lines = file.readLines() 39 | if (!lines[0].startsWith("#EXTM3U")) { 40 | Log.e(TAG, "parseChannel: Non standard m3u8 file, parsing error") 41 | return@withContext 42 | } 43 | var id = "" 44 | var name = "" 45 | var group = "" 46 | var logo = "" 47 | lines.forEach { line -> 48 | if (line.startsWith("#EXTINF")) { 49 | line.split(" ") 50 | Regex("tvg-id=\"([^\"]+)\"").find(line)?.groups?.get(1)?.let { 51 | id = it.value 52 | } 53 | Regex("tvg-logo=\"(.*?)\"").find(line)?.groups?.get(1)?.let { 54 | logo = it.value 55 | } 56 | Regex("group-title=\"([^\"]*)\"").find(line)?.groups?.get(1)?.let { 57 | group = it.value 58 | } 59 | name = line.substring(line.indexOf(",") + 1) 60 | } else if (line.startsWith("http")) { 61 | channelSet.add(TV(id, name, group, logo, url = line)) 62 | } 63 | } 64 | } 65 | } 66 | 67 | private fun getChannelFile(context: Context): File? { 68 | val file = File(context.filesDir, Constant.FILE_NAME) 69 | return if (!file.exists()) { 70 | if (downloadIPTVFile(file)) file else null 71 | } else { 72 | if (MMKV.defaultMMKV().decodeBool(Constant.KEY_AUTO_UPDATE, true)) { 73 | val lastModified = file.lastModified() 74 | if (System.currentTimeMillis() - lastModified > 24 * 60 * 60 * 1000) { 75 | try { 76 | if (downloadIPTVFile(file)) { 77 | Log.e(TAG, "getChannelFile update iptv file success") 78 | } else { 79 | Log.e(TAG, "getChannelFile update iptv file fail") 80 | } 81 | } catch (e: Exception) { 82 | e.printStackTrace() 83 | Log.e(TAG, "auto update iptv file fail") 84 | } 85 | } 86 | } 87 | file 88 | } 89 | } 90 | 91 | fun downloadIPTVFile(file: File): Boolean { 92 | val client = 93 | OkHttpClient.Builder().followRedirects(false).addInterceptor(RetryInterceptor()).build() 94 | val url = API.URL_M3U 95 | return client.newCall(Request.Builder().url(url).build()).execute().use { 96 | if (it.isSuccessful) { 97 | file.createNewFile() 98 | it.body?.let { body -> 99 | file.writeText(body.string()) 100 | true 101 | } ?: false 102 | } else { 103 | Log.e(TAG, "download iptv file fail${it.message}}") 104 | false 105 | } 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/Constant.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv 2 | 3 | /** 4 | * 5 | * Copyright (C), 2024 Potato-66, All rights reserved. 6 | * 创建时间: 2024/2/26 7 | * @since 1.0 8 | * @version 1.0 9 | * @author Potato-66 10 | */ 11 | object Constant { 12 | const val FILE_NAME = "IPTV.m3u" 13 | const val KEY_LAST_CHANNEL = "last_channel" 14 | const val KEY_BOOT_STARTUP = "boot_startup" 15 | const val KEY_AUTO_UPDATE = "auto_update" 16 | const val KEY_INIT = "init" 17 | const val KEY_FONT_SIZE = "font_size" 18 | const val DIALOG_TAG_SETTING = "setting" 19 | const val DIALOG_TAG_CHANNEL = "channel" 20 | const val DIALOG_TAG_UPDATE = "update" 21 | const val APK_NAME = "SimpleTV.apk" 22 | const val FONT_SIZE_NORMAL = 1.0f 23 | const val FONT_SIZE_LARGE = 1.2f 24 | const val FONT_SIZE_HUGE = 1.4f 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/DownloadStatus.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv 2 | 3 | /** 4 | * 5 | * Copyright (C), 2024 Potato-66, All rights reserved. 6 | * 创建时间: 2024/3/20 7 | * @since 1.0 8 | * @version 1.0 9 | * @author Potato-66 10 | */ 11 | sealed class DownloadStatus { 12 | data class DownLoading(val progress: Int) : DownloadStatus() 13 | data class Fail(val code: Int) : DownloadStatus() 14 | data class Error(val error: Throwable) : DownloadStatus() 15 | data object Success : DownloadStatus() 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/FontSizeActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.view.Window 6 | import android.view.WindowManager 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.databinding.DataBindingUtil 9 | import com.tencent.mmkv.MMKV 10 | import com.ww.simpletv.adapter.FontSizeAdapter 11 | import com.ww.simpletv.databinding.ActivityFontSizeBinding 12 | 13 | /** 14 | * 15 | * Copyright (C), 2024 Potato-66, All rights reserved. 16 | * 创建时间: 2024/3/19 17 | * @since 1.0 18 | * @version 1.0 19 | * @author Potato-66 20 | */ 21 | class FontSizeActivity : AppCompatActivity() { 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | val lp = window.attributes 26 | lp.width = (resources.displayMetrics.widthPixels * 0.5f).toInt() 27 | lp.width = WindowManager.LayoutParams.WRAP_CONTENT 28 | lp.alpha = 0.9f 29 | window.attributes = lp 30 | supportRequestWindowFeature(Window.FEATURE_NO_TITLE) 31 | val binding = DataBindingUtil.setContentView( 32 | this, 33 | R.layout.activity_font_size 34 | ) 35 | val font = MMKV.defaultMMKV().decodeFloat(Constant.KEY_FONT_SIZE, Constant.FONT_SIZE_NORMAL) 36 | val index = when (font) { 37 | Constant.FONT_SIZE_NORMAL -> 0 38 | Constant.FONT_SIZE_LARGE -> 1 39 | else -> 2 40 | } 41 | val data = resources.getStringArray(R.array.list_font_size).asList() 42 | val adapter = FontSizeAdapter(this, data) 43 | binding.lvMenu.adapter = adapter 44 | binding.lvMenu.setItemChecked(index, true) 45 | binding.lvMenu.setOnItemClickListener { _, _, position, _ -> 46 | val fontScale = when (position) { 47 | 0 -> Constant.FONT_SIZE_NORMAL 48 | 1 -> Constant.FONT_SIZE_LARGE 49 | else -> Constant.FONT_SIZE_HUGE 50 | } 51 | if (font != fontScale) { 52 | MMKV.defaultMMKV().encode(Constant.KEY_FONT_SIZE, fontScale) 53 | AppUtils.setFontScale(this, fontScale) 54 | AppUtils.recreateActivity(this) 55 | } 56 | finish() 57 | } 58 | } 59 | 60 | override fun attachBaseContext(newBase: Context?) { 61 | AppUtils.setFontScale(newBase, 1.0f) 62 | super.attachBaseContext(newBase) 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.util.Log 6 | import android.view.KeyEvent 7 | import android.view.View 8 | import android.view.WindowManager 9 | import android.widget.Toast 10 | import androidx.databinding.DataBindingUtil 11 | import androidx.lifecycle.lifecycleScope 12 | import com.tencent.mmkv.MMKV 13 | import com.ww.simpletv.databinding.ActivityMainBinding 14 | import com.ww.simpletv.dialog.SettingDialog 15 | import kotlinx.coroutines.CoroutineExceptionHandler 16 | import kotlinx.coroutines.launch 17 | 18 | /** 19 | * 20 | * Copyright (C), 2024 Potato-66, All rights reserved. 21 | * 创建时间: 2024/3/6 22 | * @since 1.0 23 | * @version 1.0 24 | * @author Potato-66 25 | */ 26 | class MainActivity : BaseActivity() { 27 | companion object{ 28 | private const val TAG = "MainActivity" 29 | } 30 | 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | super.onCreate(savedInstanceState) 33 | window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 34 | window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) 35 | val binding = 36 | DataBindingUtil.setContentView(this, R.layout.activity_main) as ActivityMainBinding 37 | val isInit = MMKV.defaultMMKV().decodeBool(Constant.KEY_INIT, true) 38 | val handler = CoroutineExceptionHandler { _, throwable -> 39 | Log.e(TAG, "onCreate: exception:${throwable.message}") 40 | if (isInit) { 41 | binding.tvMessage.text = getString(R.string.init_channel_list_fail_network_error) 42 | } else { 43 | Toast.makeText(this, R.string.update_channel_list_fail_network_error, Toast.LENGTH_LONG).show() 44 | startActivity(Intent(this@MainActivity, PlayerActivity::class.java)) 45 | finish() 46 | } 47 | binding.progress.hide() 48 | } 49 | lifecycleScope.launch(handler) { 50 | if (isInit) { 51 | binding.tvMessage.setText(R.string.init_channel_list) 52 | } 53 | ChannelUtils.parseChannel(this@MainActivity) 54 | binding.progress.hide() 55 | if (ChannelUtils.channelSet.isEmpty()) { 56 | Log.e(TAG, "onCreate: 没有频道列表,退出播放") 57 | binding.tvMessage.text = getString(R.string.load_channel_list_fail) 58 | } else { 59 | if (isInit) { 60 | MMKV.defaultMMKV().encode(Constant.KEY_INIT, false) 61 | } 62 | Log.e(TAG, "onCreate: channel size:${ChannelUtils.channelSet.size}") 63 | binding.tvMessage.visibility = View.GONE 64 | startActivity(Intent(this@MainActivity, PlayerActivity::class.java)) 65 | finish() 66 | } 67 | } 68 | binding.main.setOnClickListener { 69 | SettingDialog().show(supportFragmentManager, Constant.DIALOG_TAG_SETTING) 70 | } 71 | } 72 | 73 | override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { 74 | if (keyCode == KeyEvent.KEYCODE_MENU) { 75 | SettingDialog().show(supportFragmentManager, Constant.DIALOG_TAG_SETTING) 76 | return true 77 | } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B) { 78 | finish() 79 | } 80 | return super.onKeyDown(keyCode, event) 81 | } 82 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/PlayerActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.os.Handler 6 | import android.os.Looper 7 | import android.util.Log 8 | import android.util.Pair 9 | import android.view.KeyEvent 10 | import android.view.WindowManager 11 | import android.widget.Toast 12 | import androidx.annotation.OptIn 13 | import androidx.core.view.WindowCompat 14 | import androidx.core.view.WindowInsetsCompat 15 | import androidx.core.view.WindowInsetsControllerCompat 16 | import androidx.databinding.DataBindingUtil 17 | import androidx.media3.common.C 18 | import androidx.media3.common.ErrorMessageProvider 19 | import androidx.media3.common.MediaItem 20 | import androidx.media3.common.PlaybackException 21 | import androidx.media3.common.util.UnstableApi 22 | import androidx.media3.datasource.HttpDataSource.HttpDataSourceException 23 | import androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer 24 | import androidx.media3.decoder.vp9.LibvpxVideoRenderer 25 | import androidx.media3.exoplayer.DefaultRenderersFactory 26 | import androidx.media3.exoplayer.ExoPlayer 27 | import androidx.media3.exoplayer.Renderer 28 | import androidx.media3.exoplayer.audio.AudioRendererEventListener 29 | import androidx.media3.exoplayer.audio.AudioSink 30 | import androidx.media3.exoplayer.mediacodec.MediaCodecSelector 31 | import androidx.media3.exoplayer.source.DefaultMediaSourceFactory 32 | import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy 33 | import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy 34 | import androidx.media3.exoplayer.video.VideoRendererEventListener 35 | import com.google.gson.Gson 36 | import com.tencent.mmkv.MMKV 37 | import com.ww.simpletv.bean.TV 38 | import com.ww.simpletv.databinding.ActivityPlayerBinding 39 | import com.ww.simpletv.dialog.ChannelListDialog 40 | import com.ww.simpletv.dialog.SettingDialog 41 | import java.util.ArrayList 42 | 43 | /** 44 | * 45 | * Copyright (C), 2024 Potato-66, All rights reserved. 46 | * 创建时间: 2024/2/27 47 | * @since 1.0 48 | * @version 1.0 49 | * @author Potato-66 50 | */ 51 | class PlayerActivity : BaseActivity() { 52 | private lateinit var binding: ActivityPlayerBinding 53 | private var tvList = mutableListOf() 54 | private var exoPlayer: ExoPlayer? = null 55 | private var dialog: ChannelListDialog? = null 56 | private var exitFlag = false 57 | private var lastTV: TV? = null 58 | private var curTVIndex = 0 59 | private var retryCount = 0 60 | 61 | companion object { 62 | private const val MAX_RETRY_COUNT = 3 63 | private const val TAG = "PlayerActivity" 64 | } 65 | 66 | @OptIn(UnstableApi::class) 67 | override fun onCreate(savedInstanceState: Bundle?) { 68 | super.onCreate(savedInstanceState) 69 | window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 70 | window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) 71 | hideSystemUi() 72 | binding = DataBindingUtil.setContentView(this, R.layout.activity_player) 73 | tvList.addAll(ChannelUtils.channelSet.toMutableList()) 74 | Log.e(TAG, "onCreate: tvList size ${tvList.size}") 75 | val mmkv = MMKV.defaultMMKV() 76 | val default = Gson().toJson(ChannelUtils.channelSet.first()) 77 | val tvJson = mmkv.decodeString(Constant.KEY_LAST_CHANNEL, default) 78 | tvJson?.let { json -> 79 | val tv = Gson().fromJson(json, TV::class.java) 80 | tv?.let { 81 | lastTV = tv 82 | curTVIndex = tvList.indexOf(tv) 83 | val renderersFactory = object : DefaultRenderersFactory(this@PlayerActivity) { 84 | override fun buildAudioRenderers( 85 | context: Context, 86 | extensionRendererMode: Int, 87 | mediaCodecSelector: MediaCodecSelector, 88 | enableDecoderFallback: Boolean, 89 | audioSink: AudioSink, 90 | eventHandler: Handler, 91 | eventListener: AudioRendererEventListener, 92 | out: ArrayList 93 | ) { 94 | out.add(FfmpegAudioRenderer()) 95 | super.buildAudioRenderers( 96 | context, 97 | extensionRendererMode, 98 | mediaCodecSelector, 99 | enableDecoderFallback, 100 | audioSink, 101 | eventHandler, 102 | eventListener, 103 | out 104 | ) 105 | } 106 | 107 | override fun buildVideoRenderers( 108 | context: Context, 109 | extensionRendererMode: Int, 110 | mediaCodecSelector: MediaCodecSelector, 111 | enableDecoderFallback: Boolean, 112 | eventHandler: Handler, 113 | eventListener: VideoRendererEventListener, 114 | allowedVideoJoiningTimeMs: Long, 115 | out: ArrayList 116 | ) { 117 | out.add(LibvpxVideoRenderer(1000)) 118 | super.buildVideoRenderers( 119 | context, 120 | extensionRendererMode, 121 | mediaCodecSelector, 122 | enableDecoderFallback, 123 | eventHandler, 124 | eventListener, 125 | allowedVideoJoiningTimeMs, 126 | out 127 | ) 128 | } 129 | }.apply { 130 | setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) 131 | } 132 | val loadErrorHandlingPolicy: LoadErrorHandlingPolicy = 133 | object : DefaultLoadErrorHandlingPolicy() { 134 | override fun getRetryDelayMsFor(loadErrorInfo: LoadErrorHandlingPolicy.LoadErrorInfo): Long { 135 | val errorCount = loadErrorInfo.errorCount 136 | val exception = loadErrorInfo.exception 137 | Log.e(TAG, "getRetryDelayMsFor:errorCount:$errorCount exception:${exception::class.java.simpleName}") 138 | return if (exception is HttpDataSourceException) { 139 | 5000 140 | } else { 141 | C.TIME_UNSET 142 | } 143 | } 144 | } 145 | exoPlayer = ExoPlayer.Builder(this@PlayerActivity, renderersFactory) 146 | .setMediaSourceFactory( 147 | DefaultMediaSourceFactory(this) 148 | .setLoadErrorHandlingPolicy(loadErrorHandlingPolicy) 149 | ) 150 | .build() 151 | exoPlayer?.run { 152 | playWhenReady = true 153 | setMediaItem(MediaItem.fromUri(tv.url)) 154 | prepare() 155 | binding.exoPlay.player = this 156 | } 157 | // binding.exoPlay.setShutterBackgroundColor(Color.TRANSPARENT) 158 | } 159 | } 160 | binding.exoPlay.setOnClickListener { 161 | showChannelList() 162 | } 163 | binding.exoPlay.setOnLongClickListener { 164 | SettingDialog().show(supportFragmentManager, Constant.DIALOG_TAG_SETTING) 165 | true 166 | } 167 | binding.exoPlay.setErrorMessageProvider(object : ErrorMessageProvider { 168 | override fun getErrorMessage(throwable: PlaybackException): Pair { 169 | Log.e(TAG, "getErrorMessage: code:${throwable.errorCode} name:${throwable.errorCodeName} retryCount:$retryCount") 170 | when (throwable.errorCode) { 171 | PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, 172 | PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT -> { 173 | return Pair.create(0, getString(R.string.play_network_error_hint)) 174 | } 175 | 176 | PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED, 177 | PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, 178 | PlaybackException.ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES -> { 179 | return Pair.create(0, getString(R.string.play_decode_error_hint)) 180 | } 181 | 182 | PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, 183 | PlaybackException.ERROR_CODE_DECODING_FAILED -> { 184 | return if (retryCount < MAX_RETRY_COUNT) { 185 | retryCount++ 186 | exoPlayer?.prepare() 187 | Pair.create(0, "") 188 | } else { 189 | Pair.create(0, getString(R.string.play_decode_error_hint)) 190 | } 191 | } 192 | 193 | PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> { 194 | exoPlayer?.prepare() 195 | return Pair.create(0, "") 196 | } 197 | 198 | else -> return if (retryCount < MAX_RETRY_COUNT) { 199 | retryCount++ 200 | exoPlayer?.prepare() 201 | Pair.create(0, "") 202 | } else { 203 | Pair.create(0, getString(R.string.play_other_error_hint)) 204 | } 205 | } 206 | } 207 | }) 208 | } 209 | 210 | private fun hideSystemUi() { 211 | WindowCompat.setDecorFitsSystemWindows(window, false) 212 | WindowCompat.getInsetsController(window, window.decorView).run { 213 | hide(WindowInsetsCompat.Type.systemBars()) 214 | systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE 215 | } 216 | } 217 | 218 | override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { 219 | Log.e(TAG, "onKeyDown: $keyCode") 220 | when (keyCode) { 221 | KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_BUTTON_SELECT, 222 | KeyEvent.KEYCODE_BUTTON_A, KeyEvent.KEYCODE_NUMPAD_ENTER -> { 223 | showChannelList() 224 | return true 225 | } 226 | 227 | KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_BUTTON_B -> { 228 | if (dialog?.isVisible == true) { 229 | dialog?.dismiss() 230 | } else if (exitFlag) { 231 | finish() 232 | } else { 233 | exitFlag = true 234 | Toast.makeText(this, R.string.exit_hint, Toast.LENGTH_SHORT).show() 235 | Handler(Looper.getMainLooper()).postDelayed({ 236 | exitFlag = false 237 | }, 1500) 238 | } 239 | return true 240 | } 241 | 242 | KeyEvent.KEYCODE_DPAD_UP -> { 243 | if (curTVIndex == 0) { 244 | curTVIndex = tvList.size - 1 245 | } else { 246 | curTVIndex-- 247 | } 248 | lastTV = tvList[curTVIndex] 249 | exoPlayer?.run { 250 | setMediaItem(MediaItem.fromUri(tvList[curTVIndex].url)) 251 | prepare() 252 | } 253 | return true 254 | } 255 | 256 | KeyEvent.KEYCODE_DPAD_DOWN -> { 257 | if (curTVIndex == tvList.size - 1) { 258 | curTVIndex = 0 259 | } else { 260 | curTVIndex++ 261 | } 262 | lastTV = tvList[curTVIndex] 263 | exoPlayer?.run { 264 | setMediaItem(MediaItem.fromUri(tvList[curTVIndex].url)) 265 | prepare() 266 | } 267 | return true 268 | } 269 | 270 | KeyEvent.KEYCODE_MENU -> { 271 | SettingDialog().show(supportFragmentManager, Constant.DIALOG_TAG_SETTING) 272 | return true 273 | } 274 | 275 | } 276 | return super.onKeyDown(keyCode, event) 277 | } 278 | 279 | private fun showChannelList() { 280 | dialog = ChannelListDialog(ChannelUtils.channelSet, lastTV) 281 | dialog?.onChoose = { tv -> 282 | lastTV = tv 283 | curTVIndex = tvList.indexOf(tv) 284 | retryCount = 0 285 | exoPlayer?.run { 286 | setMediaItem(MediaItem.fromUri(tv.url)) 287 | prepare() 288 | } 289 | } 290 | dialog?.show(supportFragmentManager, Constant.DIALOG_TAG_CHANNEL) 291 | } 292 | 293 | override fun onStart() { 294 | super.onStart() 295 | exoPlayer?.run { 296 | playWhenReady = true 297 | } 298 | } 299 | 300 | override fun onPause() { 301 | super.onPause() 302 | lastTV?.let { 303 | MMKV.defaultMMKV().encode(Constant.KEY_LAST_CHANNEL, Gson().toJson(lastTV)) 304 | } 305 | } 306 | 307 | override fun onStop() { 308 | super.onStop() 309 | exoPlayer?.run { 310 | playWhenReady = false 311 | } 312 | } 313 | 314 | override fun onDestroy() { 315 | super.onDestroy() 316 | exoPlayer?.run { 317 | release() 318 | } 319 | } 320 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/RetryInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv 2 | 3 | import android.util.Log 4 | import okhttp3.Interceptor 5 | import okhttp3.Response 6 | 7 | class RetryInterceptor : Interceptor { 8 | override fun intercept(chain: Interceptor.Chain): Response { 9 | Log.e("RetryInterceptor", "intercept:download github iptv file fail, retry") 10 | val request = chain.request() 11 | val url = API.URL_IPTV 12 | val newRequest = request.newBuilder().url(url).build() 13 | return chain.proceed(newRequest) 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/TVApplication.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv 2 | 3 | import android.app.Application 4 | import com.tencent.mmkv.MMKV 5 | 6 | /** 7 | * 8 | * Copyright (C), 2024 Potato-66, All rights reserved. 9 | * 创建时间: 2024/2/27 10 | * @since 1.0 11 | * @version 1.0 12 | * @author Potato-66 13 | */ 14 | class TVApplication : Application() { 15 | override fun onCreate() { 16 | super.onCreate() 17 | MMKV.initialize(this) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/TVBootReceive.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.util.Log 7 | import com.tencent.mmkv.MMKV 8 | 9 | /** 10 | * 11 | * Copyright (C), 2024 Potato-66, All rights reserved. 12 | * 创建时间: 2024/2/28 13 | * @since 1.0 14 | * @version 1.0 15 | * @author Potato-66 16 | */ 17 | class TVBootReceive : BroadcastReceiver() { 18 | override fun onReceive(context: Context?, intent: Intent?) { 19 | Log.e("TVBootReceive", "onReceive: 开机") 20 | intent?.run { 21 | if (action == Intent.ACTION_BOOT_COMPLETED) { 22 | if (MMKV.defaultMMKV().decodeBool(Constant.KEY_BOOT_STARTUP, false)) { 23 | context?.run { 24 | startActivity(Intent(this, MainActivity::class.java).apply { 25 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 26 | }) 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/adapter/ChannelAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv.adapter 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.BaseAdapter 8 | import android.widget.ImageView 9 | import android.widget.TextView 10 | import com.bumptech.glide.Glide 11 | import com.bumptech.glide.request.RequestOptions 12 | import com.ww.simpletv.R 13 | import com.ww.simpletv.bean.TV 14 | 15 | /** 16 | * 17 | * Copyright (C), 2024 Potato-66, All rights reserved. 18 | * 创建时间: 2024/2/27 19 | * @since 1.0 20 | * @version 1.0 21 | * @author Potato-66 22 | */ 23 | class ChannelAdapter(private val context: Context) : BaseAdapter() { 24 | private val tvs = arrayListOf() 25 | 26 | fun setChannelList(tvs: List) { 27 | if (tvs.isEmpty()) { 28 | return 29 | } 30 | this.tvs.clear() 31 | this.tvs.addAll(tvs) 32 | notifyDataSetChanged() 33 | } 34 | 35 | fun getChannelList(): List = tvs 36 | 37 | override fun getCount(): Int = tvs.size 38 | 39 | override fun getItem(p0: Int): TV = tvs[p0] 40 | 41 | override fun getItemId(p0: Int): Long = p0.toLong() 42 | 43 | override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View { 44 | val holder: GroupHolder 45 | var view: View? = p1 46 | if (view == null) { 47 | view = LayoutInflater.from(context).inflate(R.layout.item_channel, p2, false) 48 | holder = GroupHolder(view) 49 | } else { 50 | holder = view.tag as GroupHolder 51 | } 52 | holder.textView.text = tvs[p0].name 53 | holder.tvNumber.text = "${p0 +1}." 54 | Glide.with(context).load(tvs[p0].logo).apply(RequestOptions().placeholder(R.drawable.place_icon)) 55 | .into(holder.icon) 56 | return view!! 57 | } 58 | 59 | inner class GroupHolder(view: View) { 60 | val icon: ImageView = view.findViewById(R.id.iv_icon) 61 | val textView: TextView = view.findViewById(R.id.tv_channel) 62 | val tvNumber: TextView = view.findViewById(R.id.tv_num) 63 | 64 | init { 65 | view.tag = this 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/adapter/FontSizeAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv.adapter 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.BaseAdapter 9 | import android.widget.RadioButton 10 | import com.ww.simpletv.Constant 11 | import com.ww.simpletv.R 12 | 13 | /** 14 | * 15 | * Copyright (C), 2024 Potato-66, All rights reserved. 16 | * 创建时间: 2024/2/27 17 | * @since 1.0 18 | * @version 1.0 19 | * @author Potato-66 20 | */ 21 | class FontSizeAdapter(private val context: Context, private val groups: List) : BaseAdapter() { 22 | override fun getCount(): Int = groups.size 23 | 24 | override fun getItem(p0: Int): String = groups[p0] 25 | 26 | override fun getItemId(p0: Int): Long = p0.toLong() 27 | 28 | override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View { 29 | val holder: GroupHolder 30 | var view: View? = p1 31 | if (view == null) { 32 | view = LayoutInflater.from(context).inflate(R.layout.item_font_size, p2, false) 33 | holder = GroupHolder(view) 34 | } else { 35 | holder = view.tag as GroupHolder 36 | } 37 | holder.radioButton.text = groups[p0] 38 | holder.radioButton.textSize = when (p0) { 39 | 0 -> 15 * Constant.FONT_SIZE_NORMAL 40 | 1 -> 15 * Constant.FONT_SIZE_LARGE 41 | else -> 15 * Constant.FONT_SIZE_HUGE 42 | } 43 | return view!! 44 | } 45 | 46 | inner class GroupHolder(view: View) { 47 | val radioButton: RadioButton = view.findViewById(R.id.rb_item) 48 | 49 | init { 50 | view.tag = this 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/adapter/GroupAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv.adapter 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.BaseAdapter 8 | import android.widget.TextView 9 | import com.ww.simpletv.R 10 | 11 | /** 12 | * 13 | * Copyright (C), 2024 Potato-66, All rights reserved. 14 | * 创建时间: 2024/2/27 15 | * @since 1.0 16 | * @version 1.0 17 | * @author Potato-66 18 | */ 19 | class GroupAdapter(private val context: Context, private val groups: List) : BaseAdapter() { 20 | override fun getCount(): Int = groups.size 21 | 22 | override fun getItem(p0: Int): String = groups[p0] 23 | 24 | override fun getItemId(p0: Int): Long = p0.toLong() 25 | 26 | override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View { 27 | val holder: GroupHolder 28 | var view: View? = p1 29 | if (view == null) { 30 | view = LayoutInflater.from(context).inflate(R.layout.item_group, p2, false) 31 | holder = GroupHolder(view) 32 | } else { 33 | holder = view.tag as GroupHolder 34 | } 35 | holder.textView.text = groups[p0] 36 | return view!! 37 | } 38 | 39 | inner class GroupHolder(view: View) { 40 | val textView: TextView = view.findViewById(R.id.tv_group) 41 | 42 | init { 43 | view.tag = this 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/bean/TV.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv.bean 2 | 3 | import java.io.Serializable 4 | 5 | /** 6 | * 7 | * Copyright (C), 2024 Potato-66, All rights reserved. 8 | * 创建时间: 2024/2/27 9 | * @since 1.0 10 | * @version 1.0 11 | * @author Potato-66 12 | */ 13 | data class TV(val id: String = "", val name: String = "", val group: String = "", val logo: String = "", var url: String = "") : Comparable, 14 | Serializable { 15 | 16 | override fun compareTo(other: TV): Int { 17 | return other.group.last() - this.group.last() 18 | } 19 | 20 | override fun hashCode(): Int { 21 | var result = id.hashCode() 22 | result = 31 * result + name.hashCode() 23 | result = 31 * result + group.hashCode() 24 | result = 31 * result + logo.hashCode() 25 | result = 31 * result + url.hashCode() 26 | return result 27 | } 28 | 29 | override fun equals(other: Any?): Boolean { 30 | if (this === other) return true 31 | if (javaClass != other?.javaClass) return false 32 | 33 | other as TV 34 | 35 | if (id != other.id) return false 36 | if (name != other.name) return false 37 | if (group != other.group) return false 38 | if (logo != other.logo) return false 39 | return url == other.url 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/bean/VersionInfo.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv.bean 2 | 3 | /** 4 | * 5 | * Copyright (C), 2024 Potato-66, All rights reserved. 6 | * 创建时间: 2024/3/18 7 | * @since 1.0 8 | * @version 1.0 9 | * @author Potato-66 10 | */ 11 | data class VersionInfo( 12 | val versionCode: Long, 13 | val versionName: String, 14 | val md5: String, 15 | val url: String, 16 | val iptv: String 17 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/dialog/BaseDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv.dialog 2 | 3 | import android.os.Bundle 4 | import android.view.Gravity 5 | import android.view.KeyEvent 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.view.WindowManager 10 | import androidx.databinding.DataBindingUtil 11 | import androidx.databinding.ViewDataBinding 12 | import androidx.fragment.app.DialogFragment 13 | import com.ww.simpletv.Constant 14 | 15 | /** 16 | * 17 | * Copyright (C), 2024 Potato-66, All rights reserved. 18 | * 创建时间: 2024/2/27 19 | * @since 1.0 20 | * @version 1.0 21 | * @author Potato-66 22 | */ 23 | abstract class BaseDialogFragment : DialogFragment() { 24 | lateinit var binding: T 25 | 26 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 27 | binding = DataBindingUtil.inflate(inflater, initLayoutResource(), container, false) 28 | initBindData() 29 | return binding.root 30 | } 31 | 32 | override fun onStart() { 33 | super.onStart() 34 | isCancelable = true 35 | dialog?.let { 36 | val window = it.window 37 | val params = window?.attributes 38 | params?.run { 39 | if (tag == Constant.DIALOG_TAG_UPDATE) { 40 | width = (resources.displayMetrics.widthPixels * 0.5).toInt() 41 | height = WindowManager.LayoutParams.WRAP_CONTENT 42 | gravity = Gravity.CENTER 43 | } else { 44 | width = (resources.displayMetrics.widthPixels * 0.4).toInt() 45 | height = resources.displayMetrics.heightPixels 46 | gravity = Gravity.START 47 | } 48 | dimAmount = 0f 49 | alpha = 0.9f 50 | window.attributes = params 51 | } 52 | it.setOnKeyListener { _, _, keyEvent -> 53 | if (keyEvent.keyCode == KeyEvent.KEYCODE_BACK && tag != Constant.DIALOG_TAG_UPDATE) { 54 | dismiss() 55 | true 56 | } else { 57 | false 58 | } 59 | } 60 | } 61 | } 62 | 63 | abstract fun initLayoutResource(): Int 64 | 65 | open fun initBindData() {} 66 | 67 | override fun onDestroy() { 68 | super.onDestroy() 69 | binding.unbind() 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/dialog/ChannelListDialog.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv.dialog 2 | 3 | import com.ww.simpletv.R 4 | import com.ww.simpletv.bean.TV 5 | import com.ww.simpletv.adapter.ChannelAdapter 6 | import com.ww.simpletv.adapter.GroupAdapter 7 | import com.ww.simpletv.databinding.DialogChannelBinding 8 | 9 | /** 10 | * 11 | * Copyright (C), 2024 Potato-66, All rights reserved. 12 | * 创建时间: 2024/2/27 13 | * @since 1.0 14 | * @version 1.0 15 | * @author Potato-66 16 | */ 17 | class ChannelListDialog(private val tvs: Set?, private val tv: TV?) : BaseDialogFragment() { 18 | var onChoose: ((tv: TV) -> Unit)? = null 19 | 20 | override fun initLayoutResource(): Int = R.layout.dialog_channel 21 | 22 | override fun onResume() { 23 | super.onResume() 24 | tvs?.let { tvs -> 25 | val map = tvs.groupBy { it.group.uppercase() } 26 | val groups = arrayListOf() 27 | groups.addAll(map.keys) 28 | binding.lvGroup.adapter = context?.let { GroupAdapter(it, groups) } 29 | var index = groups.indexOf(tv?.group?.uppercase()) 30 | if (index == -1) { 31 | index = 0 32 | } 33 | binding.lvGroup.setSelection(index) 34 | val channelAdapter = context?.let { ChannelAdapter(it) } 35 | binding.lvChannel.adapter = channelAdapter 36 | map[groups[index]]?.let { 37 | channelAdapter?.setChannelList(it) 38 | binding.lvChannel.setSelection(it.indexOf(tv)) 39 | } 40 | binding.lvGroup.setOnItemClickListener { _, _, i, _ -> 41 | map[groups[i]]?.let { 42 | channelAdapter?.setChannelList(it) 43 | binding.lvChannel.setSelection(0) 44 | } 45 | } 46 | binding.lvChannel.setOnItemClickListener { _, _, i, _ -> 47 | channelAdapter?.getChannelList()?.let { 48 | onChoose?.invoke(it[i]) 49 | } 50 | dismiss() 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/dialog/SettingDialog.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv.dialog 2 | 3 | import android.content.Intent 4 | import android.widget.Toast 5 | import androidx.lifecycle.lifecycleScope 6 | import com.tencent.mmkv.MMKV 7 | import com.ww.simpletv.AppUtils 8 | import com.ww.simpletv.ChannelUtils 9 | import com.ww.simpletv.Constant 10 | import com.ww.simpletv.FontSizeActivity 11 | import com.ww.simpletv.R 12 | import com.ww.simpletv.databinding.DialogSettingBinding 13 | import kotlinx.coroutines.CoroutineExceptionHandler 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.launch 16 | import kotlinx.coroutines.withContext 17 | import java.io.File 18 | 19 | /** 20 | * 21 | * Copyright (C), 2024 Potato-66, All rights reserved. 22 | * 创建时间: 2024/2/28 23 | * @since 1.0 24 | * @version 1.0 25 | * @author Potato-66 26 | */ 27 | class SettingDialog : BaseDialogFragment() { 28 | private var fontScale = MMKV.defaultMMKV().decodeFloat(Constant.KEY_FONT_SIZE, Constant.FONT_SIZE_NORMAL) 29 | 30 | override fun initLayoutResource(): Int = R.layout.dialog_setting 31 | 32 | override fun initBindData() { 33 | super.initBindData() 34 | binding.autoBoot = MMKV.defaultMMKV().decodeBool(Constant.KEY_BOOT_STARTUP, false) 35 | binding.autoUpdate = MMKV.defaultMMKV().decodeBool(Constant.KEY_AUTO_UPDATE, true) 36 | } 37 | 38 | override fun onResume() { 39 | super.onResume() 40 | binding.swAutoBoot.setOnCheckedChangeListener { _, b -> 41 | MMKV.defaultMMKV().encode(Constant.KEY_BOOT_STARTUP, b) 42 | } 43 | binding.swAutoUpdate.setOnCheckedChangeListener { _, b -> 44 | MMKV.defaultMMKV().encode(Constant.KEY_AUTO_UPDATE, b) 45 | } 46 | binding.btnManualUpdate.setOnClickListener { 47 | context?.let { context -> 48 | isCancelable = false 49 | binding.btnManualUpdate.text = getString(R.string.updating) 50 | binding.btnManualUpdate.isEnabled = false 51 | lifecycleScope.launch(CoroutineExceptionHandler { _, throwable -> 52 | binding.btnManualUpdate.text = getString(R.string.manual_update) 53 | binding.btnManualUpdate.isEnabled = true 54 | isCancelable = true 55 | Toast.makeText( 56 | context, 57 | "${R.string.update_file}:${throwable.message}", 58 | Toast.LENGTH_LONG 59 | ).show() 60 | }) { 61 | val b = withContext(Dispatchers.IO) { 62 | ChannelUtils.downloadIPTVFile(File(context.filesDir, Constant.FILE_NAME)) 63 | ChannelUtils.updateChannel(context) 64 | } 65 | binding.btnManualUpdate.text = getString(R.string.manual_update) 66 | binding.btnManualUpdate.isEnabled = true 67 | isCancelable = true 68 | Toast.makeText( 69 | context, 70 | if (b) R.string.update_success else R.string.update_file, 71 | Toast.LENGTH_LONG 72 | ).show() 73 | } 74 | } 75 | } 76 | context?.let { 77 | binding.tvVersion.text = getString(R.string.version_info, AppUtils.getAppVersionName(it)) 78 | } 79 | binding.btnUpdateVersion.setOnClickListener { 80 | activity?.let { context -> 81 | binding.btnUpdateVersion.text = getString(R.string.updating) 82 | binding.btnUpdateVersion.isEnabled = false 83 | lifecycleScope.launch { 84 | val versionInfo = withContext(Dispatchers.IO) { 85 | AppUtils.getVersion() 86 | } 87 | binding.btnUpdateVersion.text = getString(R.string.update_version) 88 | binding.btnUpdateVersion.isEnabled = true 89 | versionInfo?.let { 90 | if (it.versionCode > AppUtils.getAppVersionCode(context)) { 91 | UpdateDialog(it).show(context.supportFragmentManager, Constant.DIALOG_TAG_UPDATE) 92 | } else { 93 | Toast.makeText(context, getString(R.string.latest_version), Toast.LENGTH_LONG).show() 94 | } 95 | } ?: let { 96 | Toast.makeText(context, getString(R.string.get_version_error_hint), Toast.LENGTH_LONG).show() 97 | } 98 | } 99 | } 100 | } 101 | val fontScale = MMKV.defaultMMKV().decodeFloat(Constant.KEY_FONT_SIZE, Constant.FONT_SIZE_NORMAL) 102 | if (this.fontScale != fontScale) { 103 | this.fontScale = fontScale 104 | activity?.run { 105 | AppUtils.recreateActivity(this) 106 | startActivity(Intent(this,FontSizeActivity::class.java)) 107 | } 108 | } 109 | binding.tvFontSize.text = 110 | when (fontScale) { 111 | Constant.FONT_SIZE_NORMAL -> getString(R.string.font_size_normal) 112 | Constant.FONT_SIZE_LARGE -> getString(R.string.font_size_large) 113 | else -> getString(R.string.font_size_huge) 114 | } 115 | binding.rlFontSize.setOnClickListener { 116 | context?.run { 117 | startActivity(Intent(this, FontSizeActivity::class.java)) 118 | } 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ww/simpletv/dialog/UpdateDialog.kt: -------------------------------------------------------------------------------- 1 | package com.ww.simpletv.dialog 2 | 3 | import android.view.View 4 | import androidx.lifecycle.lifecycleScope 5 | import com.ww.simpletv.AppUtils 6 | import com.ww.simpletv.DownloadStatus 7 | import com.ww.simpletv.R 8 | import com.ww.simpletv.bean.VersionInfo 9 | import com.ww.simpletv.databinding.DialogUpdateBinding 10 | import kotlinx.coroutines.launch 11 | 12 | /** 13 | * 14 | * Copyright (C), 2024 Potato-66, All rights reserved. 15 | * 创建时间: 2024/3/19 16 | * @since 1.0 17 | * @version 1.0 18 | * @author Potato-66 19 | */ 20 | class UpdateDialog(private val versionInfo: VersionInfo?) : BaseDialogFragment() { 21 | override fun initLayoutResource(): Int = R.layout.dialog_update 22 | 23 | override fun onResume() { 24 | super.onResume() 25 | isCancelable = false 26 | binding.btnCancel.setOnClickListener { dismiss() } 27 | binding.btnConfirm.setOnClickListener { 28 | binding.btnConfirm.visibility = View.GONE 29 | binding.btnCancel.visibility = View.GONE 30 | binding.btnCancel.isEnabled = false 31 | binding.btnCancel.text = getString(R.string.confirm) 32 | binding.progress.visibility = View.VISIBLE 33 | binding.tvHint.visibility = View.GONE 34 | context?.let { context -> 35 | lifecycleScope.launch { 36 | context.externalCacheDir?.let { dir -> 37 | AppUtils.download(dir, versionInfo).collect { status -> 38 | when (status) { 39 | is DownloadStatus.DownLoading -> { 40 | binding.progress.progress = status.progress 41 | } 42 | 43 | is DownloadStatus.Error -> { 44 | binding.progress.visibility = View.GONE 45 | binding.tvHint.visibility = View.VISIBLE 46 | binding.btnCancel.visibility = View.VISIBLE 47 | binding.tvHint.text = 48 | getString(R.string.update_fail_hint, "error:${status.error.message}") 49 | } 50 | 51 | is DownloadStatus.Fail -> { 52 | binding.progress.visibility = View.GONE 53 | binding.tvHint.visibility = View.VISIBLE 54 | binding.btnCancel.visibility = View.VISIBLE 55 | binding.tvHint.text = 56 | getString(R.string.update_fail_hint, "fail:${status.code}") 57 | } 58 | 59 | DownloadStatus.Success -> { 60 | dismiss() 61 | AppUtils.installApk(context) 62 | } 63 | } 64 | } 65 | } 66 | binding.btnCancel.isEnabled = true 67 | } 68 | } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/arrow.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/btn_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/focus_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_logo.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/item_focus_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/place_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/simple_tv_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/switch_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/switch_thumb_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/switch_track_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_font_size.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 14 | 15 | 23 | 24 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 27 | 28 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_player.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_channel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 17 | 18 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_setting.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 15 | 16 | 17 | 21 | 22 | 27 | 28 | 35 | 36 | 49 | 50 | 63 | 64 | 77 | 78 | 84 | 85 | 93 | 94 | 101 | 102 | 103 | 104 |