├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── fr.yml └── workflows │ └── build.yml ├── .gitignore ├── HISTORY.md ├── LICENSE ├── Makefile ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── libs │ └── lib-decoder-ffmpeg-release.aar ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── lizongying │ │ └── mytv0 │ │ ├── BootReceiver.kt │ │ ├── ChannelFragment.kt │ │ ├── ConfirmationFragment.kt │ │ ├── ErrorFragment.kt │ │ ├── Ext.kt │ │ ├── GroupAdapter.kt │ │ ├── ImageHelper.kt │ │ ├── InfoFragment.kt │ │ ├── InitializerProvider.kt │ │ ├── ListAdapter.kt │ │ ├── LoadingFragment.kt │ │ ├── MainActivity.kt │ │ ├── MainViewModel.kt │ │ ├── MenuFragment.kt │ │ ├── ModalFragment.kt │ │ ├── MyTVApplication.kt │ │ ├── MyTVExceptionHandler.kt │ │ ├── PlayerFragment.kt │ │ ├── PortUtil.kt │ │ ├── ProgramAdapter.kt │ │ ├── ProgramFragment.kt │ │ ├── QrCodeUtil.kt │ │ ├── Response.kt │ │ ├── SP.kt │ │ ├── SettingFragment.kt │ │ ├── SimpleServer.kt │ │ ├── SourcesAdapter.kt │ │ ├── SourcesFragment.kt │ │ ├── TimeFragment.kt │ │ ├── UpdateManager.kt │ │ ├── Utils.kt │ │ ├── data │ │ ├── EPG.kt │ │ ├── Global.kt │ │ ├── ReleaseResponse.kt │ │ ├── ReqSettings.kt │ │ ├── ReqSources.kt │ │ ├── RespSettings.kt │ │ ├── Source.kt │ │ ├── SourceType.kt │ │ └── TV.kt │ │ ├── models │ │ ├── EPGXmlParser.kt │ │ ├── Sources.kt │ │ ├── TVGroupModel.kt │ │ ├── TVListModel.kt │ │ └── TVModel.kt │ │ └── requests │ │ ├── DnsCache.kt │ │ ├── HttpClient.kt │ │ └── Tls12SocketFactory.kt │ └── res │ ├── color │ ├── switch_thumb_color.xml │ └── switch_track_color.xml │ ├── drawable │ ├── appreciate.png │ ├── banner0.png │ ├── baseline_done_24.xml │ ├── baseline_favorite_24.xml │ ├── baseline_favorite_border_24.xml │ ├── baseline_sentiment_dissatisfied_24.xml │ ├── custom_progress_drawable.xml │ ├── light_mode_24px.xml │ ├── logo0.png │ ├── rounded_dark_bottom.xml │ ├── rounded_dark_left.xml │ ├── rounded_dark_right.xml │ ├── rounded_light_bottom.xml │ ├── rounded_white_left.xml │ ├── rounded_white_right.xml │ ├── rounded_white_top.xml │ ├── volume_off_24px.xml │ └── volume_up_24px.xml │ ├── layout │ ├── activity_main.xml │ ├── channel.xml │ ├── error.xml │ ├── group_item.xml │ ├── info.xml │ ├── list_item.xml │ ├── loading.xml │ ├── menu.xml │ ├── modal.xml │ ├── player.xml │ ├── program.xml │ ├── program_item.xml │ ├── setting.xml │ ├── settings_web.xml │ ├── show.xml │ ├── sources.xml │ ├── sources_item.xml │ └── time.xml │ ├── mipmap-hdpi │ └── ic_launcher.webp │ ├── mipmap-mdpi │ └── ic_launcher.webp │ ├── mipmap-xhdpi │ └── ic_launcher.webp │ ├── mipmap-xxhdpi │ └── ic_launcher.webp │ ├── mipmap-xxxhdpi │ └── ic_launcher.webp │ ├── raw │ ├── channels.txt │ ├── index.html │ ├── mobile.txt │ └── sources.txt │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ └── network.xml ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── history.sh ├── screenshots ├── Screenshot_20240810_151748.png ├── Screenshot_20240813_232847.png ├── Screenshot_20240813_232900.png ├── appreciate.png └── zfb.jpg ├── settings.gradle.kts └── version.json /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: 問題反饋 2 | description: 使用過程中遇到的錯誤或不合理功能 3 | labels: [ "bug" ] 4 | title: "[BUG] " 5 | body: 6 | - type: checkboxes 7 | id: checklist 8 | attributes: 9 | label: 提問題前請檢查 10 | description: 確認問題依然存在 11 | options: 12 | - label: 已檢查過問題列表,沒有人提過相同問題 13 | required: true 14 | - label: 軟件的最新版本依然沒有被修復 15 | required: true 16 | 17 | - type: input 18 | id: version 19 | attributes: 20 | label: 軟件版本 21 | validations: 22 | required: true 23 | 24 | - type: input 25 | id: device 26 | attributes: 27 | label: 電視品牌 28 | validations: 29 | required: true 30 | 31 | - type: input 32 | id: android 33 | attributes: 34 | label: Android版本 35 | validations: 36 | required: true 37 | 38 | - type: textarea 39 | id: bug 40 | attributes: 41 | label: 問題描述 42 | description: 請描述下問題的具體細節 43 | placeholder: | 44 | 1. 錯誤表現 45 | 2. 如何復現 46 | 3. 想法建議 47 | validations: 48 | required: true 49 | 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/fr.yml: -------------------------------------------------------------------------------- 1 | name: 需求建議 2 | description: 一個新的功能需求或建議 3 | labels: [ "enhancement" ] 4 | title: "[FR] " 5 | body: 6 | - type: checkboxes 7 | id: checklist 8 | attributes: 9 | label: 提需求前請檢查 10 | description: 確認新功能尚未被提出 11 | options: 12 | - label: 已檢查過需求列表,沒有人提過相同需求 13 | required: true 14 | 15 | - type: textarea 16 | id: solution 17 | attributes: 18 | label: 需求建議 19 | description: 需求需要單一且具體,請不要提諸如“優化界面”、“改善穩定性”這樣模糊的需求 20 | placeholder: | 21 | 1. 功能需求 22 | 2. 實現方案 23 | validations: 24 | required: true 25 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: set up JDK 21 19 | uses: actions/setup-java@v4 20 | with: 21 | java-version: '21' 22 | distribution: 'temurin' 23 | cache: 'gradle' 24 | 25 | - name: Run build with Gradle wrapper 26 | run: ./gradlew clean && ./gradlew assembleRelease 27 | 28 | - name: Sign app APK 29 | id: sign_app 30 | uses: r0adkll/sign-android-release@v1 31 | with: 32 | releaseDirectory: app/build/outputs/apk/release 33 | alias: ${{ secrets.ALIAS }} 34 | signingKeyBase64: ${{ secrets.KEYSTORE }} 35 | keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} 36 | keyPassword: ${{ secrets.ALIAS_PASSWORD }} 37 | env: 38 | # override default build-tools version (29.0.3) -- optional 39 | BUILD_TOOLS_VERSION: "34.0.0" 40 | 41 | - name: Get History 42 | id: get_history 43 | run: | 44 | chmod +x history.sh 45 | output=$(./history.sh) 46 | echo "$output" > history.md 47 | 48 | - name: Create Release 49 | id: create_release 50 | uses: actions/create-release@v1 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | with: 54 | tag_name: ${{ github.ref }} 55 | release_name: Release ${{ github.ref }} 56 | draft: false 57 | prerelease: false 58 | body_path: history.md 59 | 60 | - name: Set Asset Name 61 | id: set_asset_name 62 | run: | 63 | VERSION_WITHOUT_V=$(echo '${{ github.ref_name }}' | sed 's/^v//') 64 | echo "asset_name=my-tv-0_${VERSION_WITHOUT_V}.apk" >> $GITHUB_ENV 65 | 66 | - name: Upload Release Asset 67 | uses: actions/upload-release-asset@v1 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | with: 71 | upload_url: ${{ steps.create_release.outputs.upload_url }} 72 | asset_path: ${{ steps.sign_app.outputs.signedReleaseFile }} 73 | asset_name: ${{ env.asset_name }} 74 | asset_content_type: application/vnd.android.package-archive 75 | 76 | # - name: Gitee Create Release 77 | # run: | 78 | # latest_commit=$(git rev-parse HEAD) 79 | # history=$(cat history.md) 80 | # curl -v POST https://gitee.com/api/v5/repos/${{ github.repository }}/releases \ 81 | # -H "Content-Type: application/json" \ 82 | # -d '{ 83 | # "access_token": "${{ secrets.GITEE_ACCESS_TOKEN}}", 84 | # "tag_name": "${{ github.ref_name }}", 85 | # "name": "Release ${{ github.ref_name }}", 86 | # "body": "'"$history"'", 87 | # "prerelease": false, 88 | # "target_commitish": "'"$latest_commit"'" 89 | # }' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | /local.properties 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | files 18 | .kotlin/ 19 | app/release/ 20 | .README.md 21 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## 更新日誌 2 | 3 | ### v1.3.9.8 4 | 5 | * 優化頻道號 6 | 7 | ### v1.3.9.7-kitkat 8 | 9 | * 遙控器左鍵/觸屏長按打開節目單 10 | 11 | ### v1.3.9.7 12 | 13 | * 修復無法退出的問題 14 | * 優化tvg-chno 15 | 16 | ### v1.3.9.6 17 | 18 | * 遙控器左鍵/觸屏長按打開節目單 19 | 20 | ### v1.3.9.3 21 | 22 | * 支持m3u設置tvg-chno 23 | 24 | ### v1.3.9.2 25 | 26 | * 優化手機上遠程配置 27 | 28 | ### v1.3.9.0 29 | 30 | * 支持socks代理 31 | * 支持m3u設置headers 32 | 33 | ### v1.3.8.17-kitkat 34 | 35 | * 優化LOGO緩存 36 | 37 | ### v1.3.8.17 38 | 39 | * 優化LOGO緩存 40 | 41 | ### v1.3.8.16-kitkat 42 | 43 | * 增加LOGO緩存 44 | * 支持設置多個EPG地址 45 | 46 | ### v1.3.8.16 47 | 48 | * 增加LOGO緩存 49 | * 支持設置多個EPG地址 50 | 51 | ### v1.3.8.15-kitkat 52 | 53 | * 修復頻道記憶失敗的問題 54 | * 打開頻道列表同時顯示組 55 | * 增加EPG緩存 56 | 57 | ### v1.3.8.15 58 | 59 | * 修復頻道記憶失敗的問題 60 | * 打開頻道列表同時顯示組 61 | * 增加EPG緩存 62 | 63 | ### v1.3.8.14 64 | 65 | * 優化重試 66 | 67 | ### v1.3.8.13 68 | 69 | * 支持切換軟解/硬解 70 | 71 | ### v1.3.8.12 72 | 73 | * 在觸屏設備上可以調節聲音和亮度 74 | 75 | ### v1.3.8.14-kitkat 76 | 77 | * 優化樣式 78 | 79 | ### v1.3.8.11-kitkat 80 | 81 | * 新增視頻源後默認選中 82 | * 優化頻道列表文字超長顯示 83 | * 解決無法導入視頻源文件的問題 84 | 85 | ### v1.3.8.11 86 | 87 | * 新增視頻源後默認選中 88 | * 優化頻道列表文字超長顯示 89 | * 解決無法導入視頻源文件的問題 90 | 91 | ### v1.3.8.10-kitkat 92 | 93 | * 修復新增視頻源時的錯誤 94 | 95 | ### v1.3.8.10 96 | 97 | * 修復新增視頻源時的錯誤 98 | 99 | ### v1.3.8.9-kitkat 100 | 101 | * 優化EPG 102 | 103 | ### v1.3.8.9 104 | 105 | * 優化EPG 106 | 107 | ### v1.3.8.8-kitkat 108 | 109 | * 通過網絡獲取默認視頻源列表 110 | 111 | ### v1.3.8.8 112 | 113 | * 通過網絡獲取默認視頻源列表 114 | 115 | ### v1.3.8.7-kitkat 116 | 117 | * 修復切換頻道不正確的問題 118 | * EPG兼容匹配 119 | * 修復一些閃退問題 120 | 121 | ### v1.3.8.7 122 | 123 | * 修復切換頻道不正確的問題 124 | * EPG兼容匹配 125 | * 修復一些閃退問題 126 | 127 | ### v1.3.8.6 128 | 129 | * 優化遠程設置視頻源 130 | 131 | ### v1.3.8.5 132 | 133 | * 修復視頻組名為空的情況 134 | 135 | ### v1.3.8.3 136 | 137 | * 修復視頻源導入成功時卻顯示錯誤的問題 138 | * 擴寬視頻源選擇界面 139 | * 視頻源按添加時間倒序排列 140 | * 加速GitHub視頻源導入 141 | 142 | ### v1.3.8.2-kitkat 143 | 144 | * 支持多源選擇 145 | 146 | ### v1.3.8.2 147 | 148 | * 支持多源選擇 149 | 150 | ### v1.3.8.1 151 | 152 | * 支持rtmp 153 | 154 | ### v1.3.8.0-kitkat 155 | 156 | * 優化遠程配置 157 | 158 | ### v1.3.8.0 159 | 160 | * 優化遠程配置 161 | 162 | ### v1.3.7.20 163 | 164 | * 解決一個可能引起閃退的問題 165 | 166 | ### v1.3.7.20-kitkat 167 | 168 | * 解決EPG和proxy無法置空的問題 169 | * 修復獲取時間錯誤問題 170 | * 可以直接配置视频源文本 171 | 172 | ### v1.3.7.19 173 | 174 | * 解决重复启动的问题 175 | 176 | ### v1.3.7.18 177 | 178 | * 解決EPG和proxy無法置空的問題 179 | 180 | ### v1.3.7.16 181 | 182 | * 修復獲取時間錯誤問題 183 | * 可以直接配置视频源文本 184 | 185 | ### v1.3.7.19-kitkat 186 | 187 | * 兼容安卓4.4 188 | 189 | ### v1.3.7.15 190 | 191 | * 修復無法配置的問題 192 | * 修復配置頁面默認EPG不顯示的問題 193 | 194 | ### v1.3.7.14 195 | 196 | * 在遠程配置二維碼基礎上加入文字 197 | 198 | ### v1.3.7.13 199 | 200 | * 修復一些崩潰問題 201 | 202 | ### v1.3.7.12 203 | 204 | * 修復txt分組錯誤問題 205 | 206 | ### v1.3.7.11 207 | 208 | * 修復閃退問題 209 | * 修復部分設備菜單異常問題 210 | 211 | ### v1.3.7.6 212 | 213 | * 增加錯誤上報 214 | 215 | ### v1.3.7 216 | 217 | * 時間顯示秒 218 | * 支持三位頻道號 219 | 220 | ### v1.3.6 221 | 222 | * 解决開機白屏问题 223 | * 恢復默認後自動播放默認頻道 224 | * 修復頻道記憶失敗的問題 225 | * 修復菜單部分情況下無法選擇的問題 226 | * 可設置EPG地址 227 | 228 | ### v1.3.5 229 | 230 | * 可配置緊湊的菜單 231 | * 其他一些細節優化 232 | 233 | ### v1.3.4 234 | 235 | * 一些樣式優化 236 | 237 | ### v1.3.3 238 | 239 | * 一些樣式優化 240 | * 不同組頻道不合併 241 | * 防止頻道文件被覆蓋 242 | * 修復不顯示全部頻道的問題 243 | 244 | ### v1.3.2 245 | 246 | * 固定遠程配置端口為34567 247 | * 優化配置樣式 248 | * 可配置是否顯示全部頻道 249 | 250 | ### v1.3.1 251 | 252 | * 恢復默認後,立即更新視頻源 253 | 254 | ### v1.3.0 255 | 256 | * 修復收藏BUG 257 | 258 | ### v1.2.9 259 | 260 | * 同頻道多視頻地址合併 261 | * 更新了默認的視頻源,已安裝過的用戶需要在配置裡恢復默認 262 | * “收藏模式”下,上下按鍵頻道只會在收藏列表裡進行切換 263 | 264 | ### v1.2.8 265 | 266 | * 修復部分視頻源無法播放的問題 267 | * 修復一些閃退問題 268 | 269 | ### v1.2.7 270 | 271 | * 簡單支持EPG 272 | * 類別樣式優化 273 | 274 | ### v1.2.6 275 | 276 | * 解決切換頻道時黑屏問題 277 | * 解決部分配置地址請求失敗的問題 278 | * 支持配置代理 279 | 280 | ### v1.2.5 281 | 282 | * 配置地址兼容處理 283 | * 部分手機設備樣式兼容處理 284 | * 遙控器左鍵不再退出頻道列表 285 | * 解決視頻源文件分組不連續的問題 286 | * 應用啟動後進入我的收藏的功能暫不可用 287 | 288 | ### v1.2.3 289 | 290 | * 修復一些無法播放的問題 291 | 292 | ### v1.2.2 293 | 294 | * 修復一些無法播放的問題 295 | * 優化頻道列表樣式 296 | 297 | ### v1.2.1 298 | 299 | * 修復樣式 300 | 301 | ### v1.2.0 302 | 303 | * 修復部分設備網絡地址獲取錯誤的問題 304 | * 恢復默認的時候會清除收藏 305 | * 修復一些崩潰問題 306 | * 手機支持收藏功能 307 | 308 | ### v1.1.9 309 | 310 | * 菜單打開時,不能打開頻道列表 311 | * 頻道號大於1000以上時兼容樣式 312 | * 增加收藏功能 313 | 314 | ### v1.1.8 315 | 316 | * 頻道列表優化 317 | 318 | ### v1.1.7 319 | 320 | * 可以通過二維碼訪問配置地址 321 | * 支持自定義請求頭 322 | * 在線升級優化 323 | * 增加恢復默認 324 | * 增加dash支持 325 | 326 | ### v1.1.6 327 | 328 | * 默認頻道超出頻道列表範圍,自動設置為0 329 | * 通過網絡配置的頻道會自動保存 330 | * 可以通過網絡配置視頻源地址 331 | * 視頻源可以配置為本地文件 332 | 333 | ### v1.1.5 334 | 335 | * 可以指定默認頻道 336 | * 內置服務器,局域網內可配置 337 | 338 | ### v1.1.4 339 | 340 | * 默認使用上次緩存視頻源 341 | * 樣式優化 342 | 343 | ### v1.1.3 344 | 345 | * 修復m3u解析錯誤 346 | 347 | ### v1.1.2 348 | 349 | * 保存配置地址 350 | * 啟動後自動更新配置 351 | * 樣式優化 352 | 353 | ### v1.1.1 354 | 355 | * 優化頻道號選台 356 | * 如果沒有圖標,顯示頻道號 357 | 358 | ### v1.1.0 359 | 360 | * 優化頻道數字顯示 361 | * 增加時間顯示 362 | * 增加時間顯示配置 363 | 364 | ### v1.0.9 365 | 366 | * 減小頻道數字文字大小 367 | * 播放時背景顏色為黑色 368 | 369 | ### v1.0.8 370 | 371 | * 點擊節目列表/菜單以外區域,自動隱藏節目列表/菜單 372 | * 解決部分情況下崩潰問題 373 | 374 | ### v1.0.7 375 | 376 | * 支持rtsp直播 377 | * 支持循環播放 378 | * 支持txt/m3u視頻源 379 | 380 | ### v1.0.6 381 | 382 | * 修復視頻可能無聲音的問題 383 | * 修復視頻可能無法播放的問題 384 | 385 | ### v1.0.5 386 | 387 | * 修復頻道配置錯誤時可能崩潰的問題 388 | * 修復更新頻道配置時可能不生效的問題 389 | * 修復圖標為空時可能崩潰的問題 390 | 391 | ### v1.0.4 392 | 393 | * 在觸屏設備上雙擊打開節目列表 394 | * 支持自動更新 395 | 396 | ### v1.0.3 397 | 398 | * 保存上次頻道 399 | 400 | ### v1.0.2 401 | 402 | * 改變部分樣式 403 | 404 | ### v1.0.1 405 | 406 | * 支持返回鍵退出 407 | * 支持基本的視頻源配置 408 | 409 | ### v1.0.0 410 | 411 | * 基本視頻播放 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 李宗英 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: gen-version 2 | 3 | all: gen-version 4 | 5 | gen-version: 6 | git describe --tags --always 7 | git describe --tags --always | sed 's/v/ /g' | sed 's/\./ /g' | sed 's/-/ /g' | awk '{print ($$1*16777216)+($$2*65536)+($$3*256)+$$4}' 8 | 9 | #make gen v=1.2.3 10 | gen: 11 | echo $(v) | sed 's/v/ /g' | sed 's/\./ /g' | sed 's/-/ /g' | awk '{print "{\"version_code\": " ($$1*16777216)+($$2*65536)+($$3*256)+$$4 ", \"version_name\": \"" "v$(v)" "\", \"apk_name\": \"" "my-tv-0_$(v).apk" "\", \"apk_url\": \"" "https://www.gitlink.org.cn/lizongying/my-tv-0/releases/download/v$(v)/my-tv-0_$(v).apk" "\"}"}' > version.json 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 我的電視·〇 2 | 3 | 電視視頻播放軟件,可以自定義視頻源 4 | 5 | [my-tv-0](https://github.com/lizongying/my-tv-0) 6 | 7 | ## 使用 8 | 9 | * 遙控器中鍵/觸屏單擊打開視頻列表 10 | * 遙控器右鍵/觸屏雙擊打開配置 11 | * 遙控器左鍵/觸屏長按打開節目單 12 | * 遙控器返回鍵關閉視頻列表/配置 13 | * 在聚焦視頻標題的時候,右鍵收藏/取消收藏 14 | * 打開配置后,選擇遠程配置,掃描二維碼可以配置視頻源等。也可以直接遠程配置地址 http://0.0.0.0:34567 15 | * 如果視頻源地址已配置,並且打開了“應用啟動后更新視頻源”后,應用啟動后會自動更新視頻源 16 | * 默認遙控器下鍵/觸屏下滑切換到下一個視頻。換台反轉打開後,邏輯相反 17 | 18 | 注意: 19 | 20 | * 遇到問題可以先考慮重啟/恢復默認/清除數據/重新安裝等方式自助解決 21 | * 視頻源可以設置為本地文件,格式如:file:///mnt/sdcard/tmp/channels.m3u 22 | /channels.m3u 23 | * 為了使用方便,只支持設置3位頻道號 24 | * 目前設置代理後,需要重啟生效。代理屬於全局代理,也就是視頻請求及其他請求都會使用代理。 25 | 26 | 目前支持的配置格式: 27 | 28 | * txt 29 | ``` 30 | 組名,#genre# 31 | 標題,視頻地址 32 | ``` 33 | * m3u 34 | ``` 35 | #EXTM3U x-tvg-url="" 36 | #EXTINF:-1 tvg-id="" tvg-chno="" tvg-name="標準標題" tvg-logo="图标" group-title="組名",標題 37 | #EXTVLCOPT:http-user-agent= 38 | #EXTVLCOPT:http-referrer= 39 | 視頻地址 40 | ``` 41 | * json 42 | ```json 43 | [ 44 | { 45 | "group": "組名", 46 | "name": "標準標題", 47 | "title": "標題", 48 | "logo": "图标", 49 | "number": "頻道號", 50 | "uris": [ 51 | "視頻地址" 52 | ], 53 | "headers": { 54 | "user-agent": "" 55 | } 56 | } 57 | ] 58 | ``` 59 | 60 | 推薦配合使用 [my-tv-server](https://github.com/lizongying/my-tv-server) 61 | 62 | 下載安裝 [releases](https://github.com/lizongying/my-tv-0/releases/) 63 | 64 | 注意,“*-kitkat”為安卓4.4兼容版本 65 | 66 | 更多下載地址 [my-tv-0](https://lyrics.run/my-tv-0.html) 67 | 68 | ![image](./screenshots/Screenshot_20240810_151748.png) 69 | ![image](./screenshots/Screenshot_20240813_232847.png) 70 | ![image](./screenshots/Screenshot_20240813_232900.png) 71 | 72 | ## 更新日誌 73 | 74 | [更新日誌](./HISTORY.md) 75 | 76 | ## 其他 77 | 78 | 建議通過ADB進行安裝: 79 | 80 | ```shell 81 | adb install my-tv-0.apk 82 | ``` 83 | 84 | 小米電視可以使用小米電視助手進行安裝 85 | 86 | ## TODO 87 | 88 | * 支持回看 89 | * 淺色菜單 90 | 91 | ## 常見問題 92 | 93 | * 為什麼遠程配置視頻源文本後,再次打開應用後又恢復到原來的配置? 94 | 95 | 如果“應用啟動后更新視頻源”開啟後,且存在視頻源地址,則會自動更新,可能會覆蓋已保存的視頻源文本。 96 | 97 | ## 讚賞 98 | 99 | ![image](./screenshots/appreciate.png) 100 | 101 | ## 感謝 102 | 103 | [live](https://github.com/fanmingming/live) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /release/ -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.io.BufferedReader 2 | 3 | plugins { 4 | alias(libs.plugins.android.application) 5 | alias(libs.plugins.kotlin.android) 6 | } 7 | 8 | android { 9 | namespace = "com.lizongying.mytv0" 10 | compileSdk = 35 11 | 12 | defaultConfig { 13 | applicationId = "com.lizongying.mytv0" 14 | minSdk = 21 15 | targetSdk = 35 16 | versionCode = getVersionCode() 17 | versionName = getVersionName() 18 | } 19 | 20 | buildFeatures { 21 | viewBinding = true 22 | } 23 | 24 | buildTypes { 25 | release { 26 | isMinifyEnabled = true 27 | proguardFiles( 28 | getDefaultProguardFile("proguard-android-optimize.txt"), 29 | "proguard-rules.pro" 30 | ) 31 | } 32 | } 33 | compileOptions { 34 | // Flag to enable support for the new language APIs 35 | // For AGP 4.1+ 36 | isCoreLibraryDesugaringEnabled = true 37 | 38 | sourceCompatibility = JavaVersion.VERSION_1_8 39 | targetCompatibility = JavaVersion.VERSION_1_8 40 | } 41 | kotlinOptions { 42 | jvmTarget = "1.8" 43 | } 44 | } 45 | 46 | fun getTag(): String { 47 | return try { 48 | val process = Runtime.getRuntime().exec("git describe --tags --always") 49 | process.waitFor() 50 | process.inputStream.bufferedReader().use(BufferedReader::readText).trim().removePrefix("v") 51 | } catch (_: Exception) { 52 | "" 53 | } 54 | } 55 | 56 | fun getVersionCode(): Int { 57 | return try { 58 | val arr = (getTag().replace(".", " ").replace("-", " ") + " 0").split(" ") 59 | arr[0].toInt() * 16777216 + arr[1].toInt() * 65536 + arr[2].toInt() * 256 + arr[3].toInt() 60 | } catch (_: Exception) { 61 | 1 62 | } 63 | } 64 | 65 | fun getVersionName(): String { 66 | return getTag().ifEmpty { 67 | "0.0.0-1" 68 | } 69 | } 70 | 71 | dependencies { 72 | // For AGP 7.4+ 73 | coreLibraryDesugaring(libs.desugar.jdk.libs) 74 | 75 | implementation(libs.media3.ui) 76 | implementation(libs.media3.exoplayer) 77 | implementation(libs.media3.exoplayer.hls) 78 | implementation(libs.media3.exoplayer.dash) 79 | implementation(libs.media3.exoplayer.rtsp) 80 | implementation(libs.media3.datasource.okhttp) 81 | implementation(libs.media3.datasource.rtmp) 82 | 83 | implementation(libs.nanohttpd) 84 | implementation(libs.gua64) 85 | implementation(libs.zxing) 86 | implementation(libs.glide) 87 | 88 | implementation(libs.gson) 89 | implementation(libs.okhttp) 90 | 91 | implementation(libs.core.ktx) 92 | implementation(libs.coroutines) 93 | 94 | implementation(libs.constraintlayout) 95 | implementation(libs.appcompat) 96 | implementation(libs.recyclerview) 97 | implementation(libs.lifecycle.viewmodel) 98 | 99 | implementation(files("libs/lib-decoder-ffmpeg-release.aar")) 100 | } -------------------------------------------------------------------------------- /app/libs/lib-decoder-ffmpeg-release.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-0/19ab99c4c2f819fdfd08b09f64219bb7cd93eaa1/app/libs/lib-decoder-ffmpeg-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 | 23 | -keep class com.lizongying.mytv0.data.** { 24 | ; 25 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 42 | 43 | 44 | 47 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/BootReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | 7 | class BootReceiver : BroadcastReceiver() { 8 | 9 | override fun onReceive(context: Context, intent: Intent) { 10 | if (SP.bootStartup) { 11 | try { 12 | context.startActivity( 13 | Intent(context, MainActivity::class.java) 14 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 15 | ) 16 | } catch (e: Exception) { 17 | e.printStackTrace() 18 | } 19 | } 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/ChannelFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import MainViewModel 4 | import android.os.Bundle 5 | import android.os.Handler 6 | import android.os.Looper 7 | import android.util.Log 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import androidx.core.view.marginEnd 12 | import androidx.core.view.marginTop 13 | import androidx.fragment.app.Fragment 14 | import androidx.lifecycle.ViewModelProvider 15 | import com.lizongying.mytv0.databinding.ChannelBinding 16 | import com.lizongying.mytv0.models.TVModel 17 | 18 | class ChannelFragment : Fragment() { 19 | private var _binding: ChannelBinding? = null 20 | private val binding get() = _binding!! 21 | 22 | private val handler = Handler(Looper.getMainLooper()) 23 | private val delay: Long = 5000 24 | private var channel = 0 25 | private var channelCount = 0 26 | 27 | private lateinit var viewModel: MainViewModel 28 | 29 | override fun onCreateView( 30 | inflater: LayoutInflater, container: ViewGroup?, 31 | savedInstanceState: Bundle? 32 | ): View { 33 | _binding = ChannelBinding.inflate(inflater, container, false) 34 | _binding!!.root.visibility = View.GONE 35 | 36 | val application = requireActivity().applicationContext as MyTVApplication 37 | 38 | binding.channel.layoutParams.width = application.px2Px(binding.channel.layoutParams.width) 39 | binding.channel.layoutParams.height = application.px2Px(binding.channel.layoutParams.height) 40 | 41 | val layoutParams = binding.channel.layoutParams as ViewGroup.MarginLayoutParams 42 | layoutParams.topMargin = application.px2Px(binding.channel.marginTop) 43 | layoutParams.marginEnd = application.px2Px(binding.channel.marginEnd) 44 | binding.channel.layoutParams = layoutParams 45 | 46 | binding.content.textSize = application.px2PxFont(binding.content.textSize) 47 | binding.time.textSize = application.px2PxFont(binding.time.textSize) 48 | 49 | binding.main.layoutParams.width = application.shouldWidthPx() 50 | binding.main.layoutParams.height = application.shouldHeightPx() 51 | 52 | return binding.root 53 | } 54 | 55 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 56 | super.onViewCreated(view, savedInstanceState) 57 | val context = requireActivity() 58 | viewModel = ViewModelProvider(context)[MainViewModel::class.java] 59 | } 60 | 61 | fun show(tvModel: TVModel) { 62 | val tv = tvModel.tv 63 | Log.i(TAG, "show $tv") 64 | handler.removeCallbacks(hideRunnable) 65 | handler.removeCallbacks(playRunnable) 66 | if (_binding != null) { 67 | binding.content.text = 68 | if (tv.number == -1) (tv.id.plus(1)).toString() else tv.number.toString() 69 | } 70 | view?.visibility = View.VISIBLE 71 | channel = 0 72 | channelCount = 0 73 | handler.postDelayed(hideRunnable, delay) 74 | } 75 | 76 | fun show(channel: Int) { 77 | Log.i(TAG, "input $channel ${this.channel}") 78 | val tv = viewModel.groupModel.getCurrent()!!.tv 79 | if (tv.id > 10 && tv.id == this.channel - 1) { 80 | this.channel = 0 81 | channelCount = 0 82 | } 83 | if (channelCount > 2) { 84 | return 85 | } 86 | channelCount++ 87 | this.channel = "${this.channel}$channel".toInt() 88 | handler.removeCallbacks(hideRunnable) 89 | handler.removeCallbacks(playRunnable) 90 | Log.d(TAG, "channelCount $channelCount") 91 | binding.content.text = "${this.channel}" 92 | view?.visibility = View.VISIBLE 93 | if (channelCount < 3) { 94 | handler.postDelayed(playRunnable, delay) 95 | } else { 96 | playNow() 97 | } 98 | } 99 | 100 | fun playNow() { 101 | handler.postDelayed(playRunnable, 0) 102 | } 103 | 104 | override fun onResume() { 105 | super.onResume() 106 | if (view?.visibility == View.VISIBLE) { 107 | handler.postDelayed(hideRunnable, delay) 108 | } 109 | } 110 | 111 | override fun onPause() { 112 | super.onPause() 113 | handler.removeCallbacks(hideRunnable) 114 | handler.removeCallbacks(playRunnable) 115 | } 116 | 117 | private val hideRunnable = Runnable { 118 | if (_binding != null) { 119 | binding.content.text = BLANK 120 | } 121 | 122 | view?.visibility = View.GONE 123 | } 124 | 125 | fun hideSelf() { 126 | channel = 0 127 | channelCount = 0 128 | handler.postDelayed(hideRunnable, 0) 129 | } 130 | 131 | private val playRunnable = Runnable { 132 | var c = channel - 1 133 | viewModel.listModel.find { it.tv.number == channel }?.let { 134 | c = it.tv.id 135 | } 136 | if ((activity as MainActivity).play(c)) { 137 | channel = 0 138 | channelCount = 0 139 | handler.postDelayed(hideRunnable, delay) 140 | } else { 141 | hideSelf() 142 | } 143 | } 144 | 145 | override fun onDestroyView() { 146 | super.onDestroyView() 147 | _binding = null 148 | } 149 | 150 | companion object { 151 | private const val TAG = "ChannelFragment" 152 | private const val BLANK = "" 153 | } 154 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/ConfirmationFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import android.app.AlertDialog 4 | import android.app.Dialog 5 | import android.os.Bundle 6 | import androidx.fragment.app.DialogFragment 7 | 8 | class ConfirmationFragment( 9 | private val listener: ConfirmationListener, 10 | private val message: String, 11 | private val update: Boolean 12 | ) : DialogFragment() { 13 | 14 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 15 | return activity?.let { 16 | val builder = AlertDialog.Builder(it) 17 | builder.setTitle(message) 18 | if (update) { 19 | builder.setMessage("确定更新吗?") 20 | .setPositiveButton( 21 | "确定" 22 | ) { _, _ -> 23 | listener.onConfirm() 24 | } 25 | .setNegativeButton( 26 | "取消" 27 | ) { _, _ -> 28 | listener.onCancel() 29 | } 30 | } else { 31 | builder.setMessage("") 32 | .setNegativeButton( 33 | "确定" 34 | ) { _, _ -> 35 | } 36 | } 37 | builder.create() 38 | } ?: throw IllegalStateException("Activity cannot be null") 39 | } 40 | 41 | interface ConfirmationListener { 42 | fun onConfirm() 43 | fun onCancel() 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/ErrorFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.core.view.marginTop 8 | import androidx.fragment.app.Fragment 9 | import com.lizongying.mytv0.databinding.ErrorBinding 10 | 11 | class ErrorFragment : Fragment() { 12 | private var _binding: ErrorBinding? = null 13 | private val binding get() = _binding!! 14 | 15 | override fun onCreateView( 16 | inflater: LayoutInflater, container: ViewGroup?, 17 | savedInstanceState: Bundle? 18 | ): View { 19 | _binding = ErrorBinding.inflate(inflater, container, false) 20 | 21 | val application = requireActivity().applicationContext as MyTVApplication 22 | 23 | binding.logo.layoutParams.width = application.px2Px(binding.logo.layoutParams.width) 24 | binding.logo.layoutParams.height = application.px2Px(binding.logo.layoutParams.height) 25 | 26 | val layoutParams = binding.msg.layoutParams as ViewGroup.MarginLayoutParams 27 | layoutParams.topMargin = application.px2Px(binding.msg.marginTop) 28 | binding.msg.layoutParams = layoutParams 29 | 30 | binding.msg.textSize = application.px2PxFont(binding.msg.textSize) 31 | 32 | _binding = ErrorBinding.inflate(inflater, container, false) 33 | return binding.root 34 | } 35 | 36 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 37 | super.onViewCreated(view, savedInstanceState) 38 | } 39 | 40 | fun setMsg(msg: String) { 41 | if (_binding != null) { 42 | binding.msg.text = msg 43 | } 44 | } 45 | 46 | override fun onDestroyView() { 47 | super.onDestroyView() 48 | _binding = null 49 | } 50 | 51 | companion object { 52 | private const val TAG = "ErrorFragment" 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/Ext.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package com.lizongying.mytv0 4 | 5 | import android.content.Context 6 | import android.content.pm.PackageInfo 7 | import android.content.pm.PackageManager 8 | import android.content.pm.Signature 9 | import android.content.pm.SigningInfo 10 | import android.os.Build 11 | import android.util.Log 12 | import android.widget.Toast 13 | import java.security.MessageDigest 14 | 15 | private const val TAG = "Extensions" 16 | 17 | private val Context.packageInfo: PackageInfo 18 | get() { 19 | val flag = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { 20 | PackageManager.GET_SIGNATURES 21 | } else { 22 | PackageManager.GET_SIGNING_CERTIFICATES 23 | } 24 | return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { 25 | packageManager.getPackageInfo(packageName, flag) 26 | } else { 27 | packageManager.getPackageInfo( 28 | packageName, 29 | PackageManager.PackageInfoFlags.of(PackageManager.GET_SIGNING_CERTIFICATES.toLong()) 30 | ) 31 | } 32 | } 33 | 34 | /** 35 | * Return the version code of the app which is defined in build.gradle. 36 | * eg:100 37 | */ 38 | val Context.appVersionCode: Long 39 | get() { 40 | val packageInfo = this.packageInfo 41 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 42 | packageInfo.longVersionCode 43 | } else { 44 | packageInfo.versionCode.toLong() 45 | } 46 | } 47 | 48 | /** 49 | * Return the version name of the app which is defined in build.gradle. 50 | * eg:1.0.0 51 | */ 52 | val Context.appVersionName: String get() = packageInfo.versionName!! 53 | 54 | val Context.appSignature: String 55 | get() { 56 | val packageInfo = this.packageInfo 57 | var sign: Signature? = null 58 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { 59 | val signatures: Array? = packageInfo.signatures 60 | if (signatures != null) { 61 | sign = signatures[0] 62 | } 63 | } else { 64 | val signingInfo: SigningInfo? = packageInfo.signingInfo 65 | if (signingInfo != null) { 66 | sign = signingInfo.apkContentsSigners[0] 67 | } 68 | } 69 | if (sign == null) { 70 | return "" 71 | } 72 | return hashSignature(sign) 73 | } 74 | 75 | private fun hashSignature(signature: Signature): String { 76 | return try { 77 | val md = MessageDigest.getInstance("MD5") 78 | md.update(signature.toByteArray()) 79 | val digest = md.digest() 80 | digest.let { it -> it.joinToString("") { "%02x".format(it) } } 81 | } catch (e: Exception) { 82 | Log.e(TAG, "Error hashing signature", e) 83 | "" 84 | } 85 | } 86 | 87 | fun String.showToast(duration: Int = Toast.LENGTH_SHORT) { 88 | MyTVApplication.getInstance().toast(this, duration) 89 | } 90 | 91 | fun Int.getString(): String { 92 | return MyTVApplication.getInstance().getString(this) 93 | } 94 | 95 | fun Int.showToast(duration: Int = Toast.LENGTH_SHORT) { 96 | this.getString().showToast(duration) 97 | } 98 | 99 | fun String.md5(): String { 100 | val md = MessageDigest.getInstance("MD5") 101 | val digest = md.digest(this.toByteArray()) 102 | return digest.joinToString("") { "%02x".format(it) } 103 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/GroupAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import android.view.KeyEvent 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.core.content.ContextCompat 10 | import androidx.core.view.marginBottom 11 | import androidx.core.view.marginStart 12 | import androidx.recyclerview.widget.LinearLayoutManager 13 | import androidx.recyclerview.widget.RecyclerView 14 | import com.lizongying.mytv0.databinding.GroupItemBinding 15 | import com.lizongying.mytv0.models.TVGroupModel 16 | import com.lizongying.mytv0.models.TVListModel 17 | 18 | 19 | class GroupAdapter( 20 | private val context: Context, 21 | private val recyclerView: RecyclerView, 22 | private var tvGroupModel: TVGroupModel, 23 | ) : 24 | RecyclerView.Adapter() { 25 | 26 | private var listener: ItemListener? = null 27 | private var focused: View? = null 28 | private var defaultFocused = false 29 | private var defaultFocus: Int = -1 30 | 31 | var visible = false 32 | 33 | private var first = true 34 | 35 | val application = context.applicationContext as MyTVApplication 36 | 37 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 38 | val inflater = LayoutInflater.from(context) 39 | val binding = GroupItemBinding.inflate(inflater, parent, false) 40 | 41 | val layoutParams = binding.title.layoutParams as ViewGroup.MarginLayoutParams 42 | layoutParams.marginStart = application.px2Px(binding.title.marginStart) 43 | layoutParams.bottomMargin = application.px2Px(binding.title.marginBottom) 44 | binding.title.layoutParams = layoutParams 45 | 46 | binding.title.textSize = application.px2PxFont(binding.title.textSize) 47 | 48 | binding.root.isFocusable = true 49 | binding.root.isFocusableInTouchMode = true 50 | return ViewHolder(context, binding) 51 | } 52 | 53 | fun focusable(able: Boolean) { 54 | recyclerView.isFocusable = able 55 | recyclerView.isFocusableInTouchMode = able 56 | if (able) { 57 | recyclerView.descendantFocusability = ViewGroup.FOCUS_BEFORE_DESCENDANTS 58 | } else { 59 | recyclerView.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS 60 | } 61 | } 62 | 63 | fun clear() { 64 | focused?.clearFocus() 65 | recyclerView.invalidate() 66 | } 67 | 68 | override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { 69 | val listTVModel = tvGroupModel.getTVListModel(position)!! 70 | val view = viewHolder.itemView 71 | 72 | if (!defaultFocused && position == defaultFocus) { 73 | view.requestFocus() 74 | defaultFocused = true 75 | } 76 | 77 | val onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> 78 | listener?.onItemFocusChange(listTVModel, hasFocus) 79 | 80 | if (hasFocus) { 81 | viewHolder.focus(true) 82 | focused = view 83 | 84 | val p = listTVModel.getGroupIndex() 85 | if (p != tvGroupModel.positionValue) { 86 | tvGroupModel.setPosition(p) 87 | } 88 | 89 | // if (visible) { 90 | // 91 | // // "position" should not be used here, as the "list" may have been filtered out. 92 | // val p = listTVModel.getGroupIndex() 93 | // Log.e(TAG, "group getGroupIndex $p") 94 | // Log.e(TAG, "group positionValue ${tvGroupModel.positionValue}") 95 | // if (p != tvGroupModel.positionValue) { 96 | // tvGroupModel.setPosition(p) 97 | // } 98 | // } else { 99 | // visible = true 100 | // } 101 | } else { 102 | viewHolder.focus(false) 103 | } 104 | } 105 | 106 | view.onFocusChangeListener = onFocusChangeListener 107 | 108 | view.setOnClickListener { _ -> 109 | listener?.onItemClicked(position) 110 | } 111 | 112 | view.setOnKeyListener { _, keyCode, event: KeyEvent? -> 113 | if (event?.action == KeyEvent.ACTION_UP) { 114 | recyclerView.postDelayed({ 115 | val oldLikeMode = tvGroupModel.isInLikeMode; 116 | tvGroupModel.isInLikeMode = position == 0 117 | if (tvGroupModel.isInLikeMode) { 118 | // R.string.favorite_mode.showToast() 119 | } else if (oldLikeMode) { 120 | // R.string.standard_mode.showToast() 121 | } 122 | }, 500) 123 | } 124 | if (event?.action == KeyEvent.ACTION_DOWN) { 125 | 126 | // If it is already the first item and you continue to move up... 127 | if (keyCode == KeyEvent.KEYCODE_DPAD_UP && position == 0) { 128 | val p = getItemCount() - 1 129 | 130 | (recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset( 131 | p, 132 | 0 133 | ) 134 | 135 | recyclerView.postDelayed({ 136 | val v = recyclerView.findViewHolderForAdapterPosition(p) 137 | v?.itemView?.isSelected = true 138 | v?.itemView?.requestFocus() 139 | }, 0) 140 | } 141 | 142 | // If it is the last item and you continue to move down... 143 | if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && position == getItemCount() - 1) { 144 | val p = 0 145 | 146 | (recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset( 147 | p, 148 | 0 149 | ) 150 | 151 | recyclerView.postDelayed({ 152 | val v = recyclerView.findViewHolderForAdapterPosition(p) 153 | v?.itemView?.isSelected = true 154 | v?.itemView?.requestFocus() 155 | }, 0) 156 | } 157 | 158 | return@setOnKeyListener listener?.onKey(keyCode) ?: false 159 | } 160 | false 161 | } 162 | 163 | viewHolder.bindTitle(listTVModel.getName()) 164 | } 165 | 166 | override fun getItemCount() = tvGroupModel.size() 167 | 168 | class ViewHolder(private val context: Context, private val binding: GroupItemBinding) : 169 | RecyclerView.ViewHolder(binding.root) { 170 | fun bindTitle(text: String) { 171 | binding.title.text = when (text) { 172 | "我的收藏" -> context.getString(R.string.my_favorites) 173 | "全部頻道" -> context.getString(R.string.all_channels) 174 | else -> text 175 | } 176 | } 177 | 178 | fun focus(hasFocus: Boolean) { 179 | if (hasFocus) { 180 | binding.title.setTextColor(ContextCompat.getColor(context, R.color.focus)) 181 | } else { 182 | binding.title.setTextColor( 183 | ContextCompat.getColor( 184 | context, 185 | R.color.title_blur 186 | ) 187 | ) 188 | } 189 | } 190 | } 191 | 192 | fun scrollToPositionAndSelect(position: Int) { 193 | val layoutManager = recyclerView.layoutManager as? LinearLayoutManager 194 | layoutManager?.let { 195 | val delay = if (first) { 196 | 100L 197 | } else { 198 | first = false 199 | 0 200 | } 201 | 202 | recyclerView.postDelayed({ 203 | val groupPosition = 204 | if (SP.showAllChannels || position == 0) position else position - 1 205 | it.scrollToPositionWithOffset(groupPosition, 0) 206 | 207 | val viewHolder = recyclerView.findViewHolderForAdapterPosition(groupPosition) 208 | viewHolder?.itemView?.apply { 209 | isSelected = true 210 | requestFocus() 211 | } 212 | }, delay) 213 | } 214 | } 215 | 216 | interface ItemListener { 217 | fun onItemFocusChange(listTVModel: TVListModel, hasFocus: Boolean) 218 | fun onItemClicked(position: Int) 219 | fun onKey(keyCode: Int): Boolean 220 | } 221 | 222 | fun setItemListener(listener: ItemListener) { 223 | this.listener = listener 224 | } 225 | 226 | fun changed() { 227 | recyclerView.post { 228 | notifyDataSetChanged() 229 | } 230 | } 231 | 232 | companion object { 233 | private const val TAG = "GroupAdapter" 234 | } 235 | } 236 | 237 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/ImageHelper.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.drawable.BitmapDrawable 6 | import android.util.Log 7 | import com.bumptech.glide.Glide 8 | import com.lizongying.mytv0.requests.HttpClient 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.withContext 11 | import java.io.File 12 | import java.util.concurrent.ConcurrentHashMap 13 | 14 | 15 | class ImageHelper(private val context: Context) { 16 | private val cacheDir = context.cacheDir 17 | 18 | private var dir: File = File(cacheDir, LOGO) 19 | private val files = ConcurrentHashMap() 20 | 21 | init { 22 | if (!dir.exists()) { 23 | dir.mkdir() 24 | } 25 | dir.listFiles()?.forEach { file -> 26 | files[file.name] = file 27 | } 28 | } 29 | 30 | private suspend fun downloadImage(url: String, file: File): Boolean { 31 | return withContext(Dispatchers.IO) { 32 | try { 33 | val request = okhttp3.Request.Builder() 34 | .url(url) 35 | .build() 36 | 37 | HttpClient.okHttpClient.newCall(request).execute().use { response -> 38 | if (!response.isSuccessful) return@withContext false 39 | response.bodyAlias()?.byteStream()?.copyTo(file.outputStream()) 40 | true 41 | } 42 | } catch (e: Exception) { 43 | // Log.e(TAG, "downloadImage error $url", e) 44 | Log.e(TAG, "downloadImage error $url") 45 | false 46 | } 47 | } 48 | } 49 | 50 | suspend fun preloadImage( 51 | key: String, 52 | urlList: List, 53 | ) { 54 | val file = files[key] 55 | if (file != null) { 56 | Log.d(TAG, "image exists ${file.absolutePath}") 57 | return 58 | } 59 | 60 | if (urlList.isEmpty()) { 61 | return 62 | } 63 | 64 | for (url in urlList) { 65 | val file = File(cacheDir, "$LOGO/$key") 66 | if (downloadImage(url, file)) { 67 | files[file.name] = file 68 | Log.d(TAG, "downloadImage success $url ${file.absolutePath}") 69 | break 70 | } 71 | } 72 | } 73 | 74 | fun loadImage( 75 | key: String, 76 | imageView: androidx.appcompat.widget.AppCompatImageView, 77 | bitmap: Bitmap, 78 | url: String, 79 | ) { 80 | val file = files[key] 81 | if (file != null) { 82 | Log.d(TAG, "image exists ${file.absolutePath}") 83 | Glide.with(context) 84 | .load(file) 85 | .fitCenter() 86 | .into(imageView) 87 | return 88 | } 89 | 90 | if (url.isEmpty()) { 91 | Log.i(TAG, "$key image bitmap") 92 | Glide.with(context) 93 | .load(bitmap) 94 | .fitCenter() 95 | .into(imageView) 96 | } else { 97 | Log.i(TAG, "$key image $url") 98 | Glide.with(context) 99 | .load(url) 100 | .placeholder(BitmapDrawable(context.resources, bitmap)) 101 | .fitCenter() 102 | .into(imageView) 103 | } 104 | } 105 | 106 | fun clearImage() { 107 | val dir = File(cacheDir, LOGO) 108 | if (dir.exists()) { 109 | dir.deleteRecursively() 110 | } 111 | } 112 | 113 | companion object { 114 | const val TAG = "ImageHelper" 115 | const val LOGO = "logo" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/InfoFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import android.os.Bundle 7 | import android.os.Handler 8 | import android.util.Log 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import androidx.core.content.ContextCompat 13 | import androidx.core.view.marginBottom 14 | import androidx.core.view.marginStart 15 | import androidx.core.view.marginTop 16 | import androidx.fragment.app.Fragment 17 | import com.lizongying.mytv0.databinding.InfoBinding 18 | import com.lizongying.mytv0.models.TVModel 19 | 20 | 21 | class InfoFragment : Fragment() { 22 | private var _binding: InfoBinding? = null 23 | private val binding get() = _binding!! 24 | 25 | private val handler = Handler() 26 | private val delay: Long = 5000 27 | 28 | override fun onCreateView( 29 | inflater: LayoutInflater, container: ViewGroup?, 30 | savedInstanceState: Bundle? 31 | ): View { 32 | _binding = InfoBinding.inflate(inflater, container, false) 33 | 34 | val application = requireActivity().applicationContext as MyTVApplication 35 | 36 | binding.info.layoutParams.width = application.px2Px(binding.info.layoutParams.width) 37 | binding.info.layoutParams.height = application.px2Px(binding.info.layoutParams.height) 38 | 39 | val layoutParams = binding.info.layoutParams as ViewGroup.MarginLayoutParams 40 | layoutParams.bottomMargin = application.px2Px(binding.info.marginBottom) 41 | binding.info.layoutParams = layoutParams 42 | 43 | binding.logo.layoutParams.width = application.px2Px(binding.logo.layoutParams.width) 44 | var padding = application.px2Px(binding.logo.paddingTop) 45 | binding.logo.setPadding(padding, padding, padding, padding) 46 | binding.main.layoutParams.width = application.px2Px(binding.main.layoutParams.width) 47 | padding = application.px2Px(binding.main.paddingTop) 48 | binding.main.setPadding(padding, padding, padding, padding) 49 | 50 | val layoutParamsMain = binding.main.layoutParams as ViewGroup.MarginLayoutParams 51 | layoutParamsMain.marginStart = application.px2Px(binding.main.marginStart) 52 | binding.main.layoutParams = layoutParamsMain 53 | 54 | val layoutParamsDesc = binding.desc.layoutParams as ViewGroup.MarginLayoutParams 55 | layoutParamsDesc.topMargin = application.px2Px(binding.desc.marginTop) 56 | binding.desc.layoutParams = layoutParamsDesc 57 | 58 | binding.title.textSize = application.px2PxFont(binding.title.textSize) 59 | binding.desc.textSize = application.px2PxFont(binding.desc.textSize) 60 | 61 | binding.container.layoutParams.width = application.shouldWidthPx() 62 | binding.container.layoutParams.height = application.shouldHeightPx() 63 | 64 | _binding!!.root.visibility = View.GONE 65 | return binding.root 66 | } 67 | 68 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 69 | super.onViewCreated(view, savedInstanceState) 70 | (activity as MainActivity).ready(TAG) 71 | } 72 | 73 | fun show(tvModel: TVModel) { 74 | // TODO make sure attached 75 | if (!isAdded) { 76 | Log.e(TAG, "Fragment not attached to a context.") 77 | return 78 | } 79 | 80 | val tv = tvModel.tv 81 | 82 | val context = requireContext() 83 | val application = context.applicationContext as MyTVApplication 84 | val imageHelper = application.imageHelper 85 | 86 | binding.title.text = tv.title 87 | 88 | when (tv.title) { 89 | else -> { 90 | val width = 300 91 | val height = 180 92 | val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) 93 | val canvas = Canvas(bitmap) 94 | 95 | val channelNum = if (tv.number == -1) tv.id.plus(1) else tv.number 96 | var size = 150f 97 | if (channelNum > 99) { 98 | size = 100f 99 | } 100 | if (channelNum > 999) { 101 | size = 75f 102 | } 103 | val paint = Paint().apply { 104 | color = ContextCompat.getColor(context, R.color.title_blur) 105 | textSize = size 106 | textAlign = Paint.Align.CENTER 107 | } 108 | val x = width / 2f 109 | val y = height / 2f - (paint.descent() + paint.ascent()) / 2 110 | canvas.drawText(channelNum.toString(), x, y, paint) 111 | 112 | val name = if (tv.name.isNotEmpty()) { tv.name } else { tv.title } 113 | imageHelper.loadImage(name, binding.logo, bitmap, tv.logo) 114 | } 115 | } 116 | 117 | val epg = tvModel.epg.value?.filter { it.beginTime < Utils.getDateTimestamp() } 118 | if (!epg.isNullOrEmpty()) { 119 | binding.desc.text = epg.last().title 120 | } else { 121 | binding.desc.text = "精彩節目" 122 | } 123 | 124 | handler.removeCallbacks(removeRunnable) 125 | view?.visibility = View.VISIBLE 126 | handler.postDelayed(removeRunnable, delay) 127 | } 128 | 129 | override fun onResume() { 130 | super.onResume() 131 | handler.postDelayed(removeRunnable, delay) 132 | } 133 | 134 | override fun onPause() { 135 | super.onPause() 136 | handler.removeCallbacks(removeRunnable) 137 | } 138 | 139 | private val removeRunnable = Runnable { 140 | view?.visibility = View.GONE 141 | } 142 | 143 | override fun onDestroyView() { 144 | super.onDestroyView() 145 | _binding = null 146 | } 147 | 148 | companion object { 149 | private const val TAG = "InfoFragment" 150 | } 151 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/InitializerProvider.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import android.content.ContentProvider 4 | import android.content.ContentValues 5 | import android.net.Uri 6 | 7 | internal class InitializerProvider : ContentProvider() { 8 | 9 | // Happens before Application#onCreate.It's fine to init something here 10 | override fun onCreate(): Boolean { 11 | SP.init(context!!) 12 | return true 13 | } 14 | 15 | override fun query( 16 | uri: Uri, 17 | projection: Array?, 18 | selection: String?, 19 | selectionArgs: Array?, 20 | sortOrder: String?, 21 | ) = unsupported() 22 | 23 | override fun getType(uri: Uri) = unsupported() 24 | 25 | override fun insert(uri: Uri, values: ContentValues?) = unsupported() 26 | 27 | override fun delete(uri: Uri, selection: String?, selectionArgs: Array?) = 28 | unsupported() 29 | 30 | override fun update( 31 | uri: Uri, 32 | values: ContentValues?, 33 | selection: String?, 34 | selectionArgs: Array?, 35 | ) = unsupported() 36 | 37 | private fun unsupported(errorMessage: String? = null): Nothing = 38 | throw UnsupportedOperationException(errorMessage) 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/LoadingFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import com.lizongying.mytv0.databinding.LoadingBinding 9 | 10 | class LoadingFragment : Fragment() { 11 | private var _binding: LoadingBinding? = null 12 | private val binding get() = _binding!! 13 | 14 | override fun onCreateView( 15 | inflater: LayoutInflater, container: ViewGroup?, 16 | savedInstanceState: Bundle? 17 | ): View { 18 | _binding = LoadingBinding.inflate(inflater, container, false) 19 | 20 | val application = requireActivity().applicationContext as MyTVApplication 21 | 22 | binding.bar.layoutParams.width = application.px2Px(binding.bar.layoutParams.width) 23 | binding.bar.layoutParams.height = application.px2Px(binding.bar.layoutParams.height) 24 | 25 | return binding.root 26 | } 27 | 28 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 29 | super.onViewCreated(view, savedInstanceState) 30 | } 31 | 32 | override fun onDestroyView() { 33 | super.onDestroyView() 34 | _binding = null 35 | } 36 | 37 | companion object { 38 | private const val TAG = "LoadingFragment" 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/ModalFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import android.content.res.Configuration 4 | import android.os.Bundle 5 | import android.os.Handler 6 | import android.os.Looper 7 | import android.util.Log 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import android.view.WindowManager 12 | import androidx.fragment.app.DialogFragment 13 | import com.bumptech.glide.Glide 14 | import com.lizongying.mytv0.Utils.getDateTimestamp 15 | import com.lizongying.mytv0.databinding.ModalBinding 16 | 17 | 18 | class ModalFragment : DialogFragment() { 19 | 20 | private var _binding: ModalBinding? = null 21 | private val binding get() = _binding!! 22 | 23 | private val handler = Handler(Looper.myLooper()!!) 24 | private val delayHideAppreciateModal = 10000L 25 | 26 | override fun onStart() { 27 | super.onStart() 28 | dialog?.window?.apply { 29 | addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) 30 | decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 31 | } 32 | } 33 | 34 | override fun onCreateView( 35 | inflater: LayoutInflater, 36 | container: ViewGroup?, 37 | savedInstanceState: Bundle? 38 | ): View { 39 | _binding = ModalBinding.inflate(inflater, container, false) 40 | return binding.root 41 | } 42 | 43 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 44 | super.onViewCreated(view, savedInstanceState) 45 | 46 | val url = arguments?.getString(KEY_URL) 47 | if (!url.isNullOrEmpty()) { 48 | val size = Utils.dpToPx(200) 49 | val u = "$url?${getDateTimestamp().toString().reversed()}" 50 | val img = QrCodeUtil().createQRCodeBitmap(u, size, size) 51 | 52 | Glide.with(requireContext()) 53 | .load(img) 54 | .into(binding.modalImage) 55 | binding.modalText.text = u.removePrefix("http://") 56 | binding.modalText.visibility = View.VISIBLE 57 | if (!isTV()) { 58 | binding.modal.setOnClickListener { 59 | try { 60 | val mainActivity = (activity as MainActivity) 61 | mainActivity.showWebViewPopup(u) 62 | handler.postDelayed(hideAppreciateModal, 0) 63 | } catch (e: Exception) { 64 | Log.e(TAG, "onViewCreated", e) 65 | } 66 | } 67 | } 68 | } else { 69 | Glide.with(requireContext()) 70 | .load(arguments?.getInt(KEY_DRAWABLE_ID)) 71 | .into(binding.modalImage) 72 | binding.modalText.visibility = View.GONE 73 | } 74 | 75 | handler.postDelayed(hideAppreciateModal, delayHideAppreciateModal) 76 | } 77 | 78 | private val hideAppreciateModal = Runnable { 79 | if (!this.isHidden) { 80 | this.dismiss() 81 | } 82 | } 83 | 84 | private fun isTV(): Boolean { 85 | val uiMode = resources.configuration.uiMode and Configuration.UI_MODE_TYPE_MASK 86 | return uiMode == Configuration.UI_MODE_TYPE_TELEVISION 87 | } 88 | 89 | override fun onDestroyView() { 90 | super.onDestroyView() 91 | _binding = null 92 | handler.removeCallbacksAndMessages(null) 93 | } 94 | 95 | companion object { 96 | const val KEY_DRAWABLE_ID = "drawable_id" 97 | const val KEY_URL = "url" 98 | const val TAG = "ModalFragment" 99 | } 100 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/MyTVApplication.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.res.Configuration 6 | import android.content.res.Resources 7 | import android.os.Build 8 | import android.os.Handler 9 | import android.os.Looper 10 | import android.util.DisplayMetrics 11 | import android.view.WindowManager 12 | import android.widget.Toast 13 | import java.util.Locale 14 | 15 | class MyTVApplication : Application() { 16 | 17 | companion object { 18 | private const val TAG = "MyTVApplication" 19 | private lateinit var instance: MyTVApplication 20 | 21 | @JvmStatic 22 | fun getInstance(): MyTVApplication { 23 | return instance 24 | } 25 | } 26 | 27 | private lateinit var displayMetrics: DisplayMetrics 28 | private lateinit var realDisplayMetrics: DisplayMetrics 29 | 30 | private var width = 0 31 | private var height = 0 32 | private var shouldWidth = 0 33 | private var shouldHeight = 0 34 | private var ratio = 1.0 35 | private var density = 2.0f 36 | private var scale = 1.0f 37 | 38 | lateinit var imageHelper:ImageHelper 39 | 40 | override fun onCreate() { 41 | super.onCreate() 42 | instance = this 43 | 44 | displayMetrics = DisplayMetrics() 45 | realDisplayMetrics = DisplayMetrics() 46 | val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager 47 | windowManager.defaultDisplay.getMetrics(displayMetrics) 48 | windowManager.defaultDisplay.getRealMetrics(realDisplayMetrics) 49 | 50 | if (realDisplayMetrics.heightPixels > realDisplayMetrics.widthPixels) { 51 | width = realDisplayMetrics.heightPixels 52 | height = realDisplayMetrics.widthPixels 53 | } else { 54 | width = realDisplayMetrics.widthPixels 55 | height = realDisplayMetrics.heightPixels 56 | } 57 | 58 | density = Resources.getSystem().displayMetrics.density 59 | scale = displayMetrics.scaledDensity 60 | 61 | if ((width.toDouble() / height) < (16.0 / 9.0)) { 62 | ratio = width * 2 / 1920.0 / density 63 | shouldWidth = width 64 | shouldHeight = (width * 9.0 / 16.0).toInt() 65 | } else { 66 | ratio = height * 2 / 1080.0 / density 67 | shouldHeight = height 68 | shouldWidth = (height * 16.0 / 9.0).toInt() 69 | } 70 | 71 | Thread.setDefaultUncaughtExceptionHandler(MyTVExceptionHandler(this)) 72 | 73 | imageHelper = ImageHelper(this) 74 | } 75 | 76 | fun getDisplayMetrics(): DisplayMetrics { 77 | return displayMetrics 78 | } 79 | 80 | fun toast(message: CharSequence = "", duration: Int = Toast.LENGTH_SHORT) { 81 | Handler(Looper.getMainLooper()).post { 82 | Toast.makeText(applicationContext, message, duration).show() 83 | } 84 | } 85 | 86 | fun shouldWidthPx(): Int { 87 | return shouldWidth 88 | } 89 | 90 | fun shouldHeightPx(): Int { 91 | return shouldHeight 92 | } 93 | 94 | fun dp2Px(dp: Int): Int { 95 | return (dp * ratio * density + 0.5f).toInt() 96 | } 97 | 98 | fun px2Px(px: Int): Int { 99 | return (px * ratio + 0.5f).toInt() 100 | } 101 | 102 | fun px2PxFont(px: Float): Float { 103 | return (px * ratio / scale).toFloat() 104 | } 105 | 106 | fun sp2Px(sp: Float): Float { 107 | return (sp * ratio * scale).toFloat() 108 | } 109 | 110 | override fun attachBaseContext(base: Context) { 111 | try { 112 | val locale = Locale.TRADITIONAL_CHINESE 113 | val config = Configuration() 114 | config.setLocale(locale) 115 | super.attachBaseContext( 116 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { 117 | base.createConfigurationContext(config) 118 | } else { 119 | val resources = base.resources 120 | resources.updateConfiguration(config, resources.displayMetrics) 121 | base 122 | } 123 | ) 124 | } catch (_: Exception) { 125 | super.attachBaseContext(base) 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/MyTVExceptionHandler.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.util.Log 6 | import com.lizongying.mytv0.requests.HttpClient 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.launch 9 | import kotlinx.coroutines.runBlocking 10 | import kotlinx.coroutines.withContext 11 | import okhttp3.MediaType.Companion.toMediaType 12 | import okhttp3.RequestBody.Companion.toRequestBody 13 | import kotlin.system.exitProcess 14 | 15 | class MyTVExceptionHandler(val context: Context) : Thread.UncaughtExceptionHandler { 16 | override fun uncaughtException(t: Thread, e: Throwable) { 17 | val crashInfo = 18 | "APP: ${context.appVersionName}, PRODUCT: ${Build.PRODUCT}, DEVICE: ${Build.DEVICE}, SUPPORTED_ABIS: ${Build.SUPPORTED_ABIS.joinToString()}, BOARD: ${Build.BOARD}, MANUFACTURER: ${Build.MANUFACTURER}, MODEL: ${Build.MODEL}, VERSION: ${Build.VERSION.SDK_INT}\nThread: ${t.name}\nException: ${e.message}\nStackTrace: ${ 19 | Log.getStackTraceString( 20 | e 21 | ) 22 | }\n" 23 | 24 | runBlocking { 25 | launch { 26 | saveCrashInfoToFile(crashInfo) 27 | 28 | withContext(Dispatchers.Main) { 29 | android.os.Process.killProcess(android.os.Process.myPid()) 30 | exitProcess(1) 31 | } 32 | } 33 | } 34 | } 35 | 36 | private suspend fun saveCrashInfoToFile(crashInfo: String) { 37 | if (isLimit()) { 38 | Log.e(TAG, crashInfo) 39 | } else { 40 | try { 41 | saveLog(crashInfo) 42 | } catch (e: Exception) { 43 | e.printStackTrace() 44 | } 45 | } 46 | } 47 | 48 | private fun isLimit(): Boolean { 49 | if (context.appVersionName != SP.version) { 50 | SP.version = context.appVersionName 51 | SP.logTimes = SP.DEFAULT_LOG_TIMES 52 | return false 53 | } else { 54 | SP.logTimes-- 55 | return SP.logTimes < 0 56 | } 57 | } 58 | 59 | private suspend fun saveLog(crashInfo: String) { 60 | withContext(Dispatchers.IO) { 61 | try { 62 | val request = okhttp3.Request.Builder() 63 | .url("https://lyrics.run/my-tv-0/v1/log") 64 | .method("POST", crashInfo.toRequestBody("text/plain".toMediaType())) 65 | .build() 66 | 67 | HttpClient.okHttpClient.newCall(request).execute().use { response -> 68 | if (response.isSuccessful) { 69 | Log.i(TAG, "log success") 70 | } else { 71 | Log.e(TAG, "log failed: ${response.codeAlias()}") 72 | } 73 | } 74 | } catch (e: Exception) { 75 | e.printStackTrace() 76 | } 77 | } 78 | } 79 | 80 | companion object { 81 | private const val TAG = "MyTVException" 82 | } 83 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/PortUtil.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import java.net.Inet4Address 4 | import java.net.NetworkInterface 5 | 6 | object PortUtil { 7 | 8 | fun lan(): String? { 9 | val networkInterfaces = NetworkInterface.getNetworkInterfaces() 10 | while (networkInterfaces.hasMoreElements()) { 11 | val inetAddresses = networkInterfaces.nextElement().inetAddresses 12 | while (inetAddresses.hasMoreElements()) { 13 | val inetAddress = inetAddresses.nextElement() 14 | if (inetAddress is Inet4Address) { 15 | if (inetAddress.hostAddress == "127.0.0.1") { 16 | continue 17 | } 18 | return inetAddress.hostAddress 19 | } 20 | } 21 | } 22 | return null 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/ProgramAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import android.content.Context 4 | import android.view.KeyEvent 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.core.content.ContextCompat 9 | import androidx.recyclerview.widget.LinearLayoutManager 10 | import androidx.recyclerview.widget.RecyclerView 11 | import com.lizongying.mytv0.data.EPG 12 | import com.lizongying.mytv0.databinding.ProgramItemBinding 13 | 14 | 15 | class ProgramAdapter( 16 | private val context: Context, 17 | private val recyclerView: RecyclerView, 18 | private var epgList: List, 19 | private var index: Int, 20 | ) : 21 | RecyclerView.Adapter() { 22 | 23 | private var listener: ItemListener? = null 24 | private var focused: View? = null 25 | val application = context.applicationContext as MyTVApplication 26 | 27 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 28 | val inflater = LayoutInflater.from(context) 29 | val binding = ProgramItemBinding.inflate(inflater, parent, false) 30 | 31 | val textSize = application.px2PxFont(binding.title.textSize) 32 | binding.title.textSize = textSize 33 | binding.description.textSize = textSize 34 | 35 | binding.root.isFocusable = true 36 | binding.root.isFocusableInTouchMode = true 37 | return ViewHolder(context, binding) 38 | } 39 | 40 | override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { 41 | val epg = epgList[position] 42 | val view = viewHolder.itemView 43 | 44 | view.onFocusChangeListener = View.OnFocusChangeListener { v, hasFocus -> 45 | listener?.onItemFocusChange(epg, hasFocus) 46 | val isCurrent = position == index 47 | if (hasFocus) { 48 | viewHolder.focus(true, isCurrent) 49 | focused = view 50 | } else { 51 | viewHolder.focus(false, isCurrent) 52 | } 53 | } 54 | 55 | view.setOnKeyListener { _, keyCode, event: KeyEvent? -> 56 | if (event?.action == KeyEvent.ACTION_UP) { 57 | if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { 58 | return@setOnKeyListener true 59 | } 60 | } 61 | if (event?.action == KeyEvent.ACTION_DOWN) { 62 | // If it is already the first item and you continue to move up... 63 | if (keyCode == KeyEvent.KEYCODE_DPAD_UP && position == 0) { 64 | val p = getItemCount() - 1 65 | 66 | (recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset( 67 | p, 68 | 0 69 | ) 70 | 71 | recyclerView.postDelayed({ 72 | val v = recyclerView.findViewHolderForAdapterPosition(p) 73 | v?.itemView?.isSelected = true 74 | v?.itemView?.requestFocus() 75 | }, 0) 76 | } 77 | 78 | 79 | // If it is the last item and you continue to move down... 80 | if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && position == getItemCount() - 1) { 81 | val p = 0 82 | 83 | (recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset( 84 | p, 85 | 0 86 | ) 87 | 88 | recyclerView.postDelayed({ 89 | val v = recyclerView.findViewHolderForAdapterPosition(p) 90 | v?.itemView?.isSelected = true 91 | v?.itemView?.requestFocus() 92 | }, 0) 93 | } 94 | 95 | return@setOnKeyListener listener?.onKey(keyCode) == true 96 | } 97 | false 98 | } 99 | 100 | viewHolder.bindTitle(epg) 101 | } 102 | 103 | override fun getItemCount() = epgList.size 104 | 105 | class ViewHolder(private val context: Context, private val binding: ProgramItemBinding) : 106 | RecyclerView.ViewHolder(binding.root) { 107 | 108 | fun bindTitle(epg: EPG) { 109 | binding.title.text = "${Utils.getDateFormat("HH:mm", epg.beginTime)}-${ 110 | Utils.getDateFormat( 111 | "HH:mm", 112 | epg.endTime 113 | ) 114 | }" 115 | binding.description.text = epg.title 116 | } 117 | 118 | fun focus(hasFocus: Boolean, isCurrent: Boolean) { 119 | if (hasFocus) { 120 | val color = ContextCompat.getColor(context, R.color.focus) 121 | binding.title.setTextColor(color) 122 | binding.description.setTextColor(color) 123 | } else { 124 | if (isCurrent) { 125 | val color = ContextCompat.getColor(context, R.color.white) 126 | binding.title.setTextColor(color) 127 | binding.description.setTextColor(color) 128 | } else { 129 | val color = ContextCompat.getColor(context, R.color.description_blur) 130 | binding.title.setTextColor(color) 131 | binding.description.setTextColor(color) 132 | } 133 | } 134 | } 135 | } 136 | 137 | fun scrollToPositionAndSelect(position: Int) { 138 | val layoutManager = recyclerView.layoutManager as? LinearLayoutManager 139 | layoutManager?.let { 140 | recyclerView.postDelayed({ 141 | it.scrollToPositionWithOffset(position, 0) 142 | 143 | val viewHolder = recyclerView.findViewHolderForAdapterPosition(position) 144 | viewHolder?.itemView?.apply { 145 | isSelected = true 146 | requestFocus() 147 | } 148 | }, 0) 149 | } 150 | } 151 | 152 | interface ItemListener { 153 | fun onItemFocusChange(epg: EPG, hasFocus: Boolean) 154 | fun onKey(keyCode: Int): Boolean 155 | } 156 | 157 | fun setItemListener(listener: ItemListener) { 158 | this.listener = listener 159 | } 160 | 161 | companion object { 162 | private const val TAG = "ProgramAdapter" 163 | } 164 | } 165 | 166 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/ProgramFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import android.os.Bundle 4 | import android.os.Handler 5 | import android.util.Log 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.fragment.app.Fragment 10 | import androidx.lifecycle.ViewModelProvider 11 | import androidx.recyclerview.widget.LinearLayoutManager 12 | import com.lizongying.mytv0.data.EPG 13 | import com.lizongying.mytv0.databinding.ProgramBinding 14 | 15 | class ProgramFragment : Fragment(), ProgramAdapter.ItemListener { 16 | private var _binding: ProgramBinding? = null 17 | private val binding get() = _binding!! 18 | 19 | private val handler = Handler() 20 | private val delay: Long = 5000 21 | 22 | private lateinit var programAdapter: ProgramAdapter 23 | 24 | private lateinit var viewModel: MainViewModel 25 | 26 | override fun onCreateView( 27 | inflater: LayoutInflater, container: ViewGroup?, 28 | savedInstanceState: Bundle? 29 | ): View { 30 | _binding = ProgramBinding.inflate(inflater, container, false) 31 | return binding.root 32 | } 33 | 34 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 35 | super.onViewCreated(view, savedInstanceState) 36 | val context = requireActivity() 37 | viewModel = ViewModelProvider(context)[MainViewModel::class.java] 38 | 39 | binding.program.setOnClickListener { 40 | hideSelf() 41 | } 42 | 43 | onVisible() 44 | } 45 | 46 | private fun hideSelf() { 47 | requireActivity().supportFragmentManager.beginTransaction() 48 | .hide(this) 49 | .commitAllowingStateLoss() 50 | } 51 | 52 | private val hideRunnable = Runnable { 53 | hideSelf() 54 | } 55 | 56 | fun onVisible() { 57 | val context = requireActivity() 58 | 59 | viewModel.groupModel.getCurrent()?.let { 60 | val index = it.epgValue.indexOfFirst { it.endTime > Utils.getDateTimestamp() } 61 | programAdapter = ProgramAdapter( 62 | context, 63 | binding.list, 64 | it.epgValue, 65 | index, 66 | ) 67 | binding.list.adapter = programAdapter 68 | binding.list.layoutManager = LinearLayoutManager(context) 69 | 70 | programAdapter.setItemListener(this) 71 | 72 | if (index > -1) { 73 | programAdapter.scrollToPositionAndSelect(index) 74 | } 75 | 76 | handler.postDelayed(hideRunnable, delay) 77 | } 78 | } 79 | 80 | fun onHidden() { 81 | handler.removeCallbacks(hideRunnable) 82 | } 83 | 84 | override fun onHiddenChanged(hidden: Boolean) { 85 | super.onHiddenChanged(hidden) 86 | if (!hidden) { 87 | onVisible() 88 | } else { 89 | onHidden() 90 | } 91 | } 92 | 93 | override fun onPause() { 94 | super.onPause() 95 | handler.removeCallbacks(hideRunnable) 96 | } 97 | 98 | override fun onDestroyView() { 99 | super.onDestroyView() 100 | _binding = null 101 | } 102 | 103 | override fun onItemFocusChange(epg: EPG, hasFocus: Boolean) { 104 | handler.removeCallbacks(hideRunnable) 105 | handler.postDelayed(hideRunnable, delay) 106 | } 107 | 108 | override fun onKey(keyCode: Int): Boolean { 109 | return false 110 | } 111 | 112 | companion object { 113 | private const val TAG = "ProgramFragment" 114 | } 115 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/QrCodeUtil.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Color 5 | import androidx.annotation.ColorInt 6 | import com.google.zxing.BarcodeFormat 7 | import com.google.zxing.EncodeHintType 8 | import com.google.zxing.WriterException 9 | import com.google.zxing.qrcode.QRCodeWriter 10 | import java.util.Hashtable 11 | 12 | class QrCodeUtil { 13 | 14 | fun createQRCodeBitmap( 15 | content: String, 16 | width: Int, 17 | height: Int, 18 | characterSet: String = "UTF-8", 19 | errorCorrection: String = "L", 20 | margin: String = "1", 21 | @ColorInt colorBlack: Int = Color.BLACK, 22 | @ColorInt colorWhite: Int = Color.WHITE, 23 | ): Bitmap? { 24 | if (width < 0 || height < 0) { 25 | return null 26 | } 27 | try { 28 | val hints: Hashtable = Hashtable() 29 | if (characterSet.isNotEmpty()) { 30 | hints[EncodeHintType.CHARACTER_SET] = characterSet 31 | } 32 | if (errorCorrection.isNotEmpty()) { 33 | hints[EncodeHintType.ERROR_CORRECTION] = errorCorrection 34 | } 35 | if (margin.isNotEmpty()) { 36 | hints[EncodeHintType.MARGIN] = margin 37 | } 38 | val bitMatrix = 39 | QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints) 40 | 41 | val pixels = IntArray(width * height) 42 | for (y in 0 until height) { 43 | for (x in 0 until width) { 44 | if (bitMatrix[x, y]) { 45 | pixels[y * width + x] = colorBlack 46 | } else { 47 | pixels[y * width + x] = colorWhite 48 | } 49 | } 50 | } 51 | val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) 52 | bitmap.setPixels(pixels, 0, width, 0, 0, width, height) 53 | return bitmap 54 | } catch (e: WriterException) { 55 | e.printStackTrace() 56 | } 57 | return null 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/Response.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import okhttp3.Response 4 | import okhttp3.ResponseBody 5 | 6 | 7 | fun Response.bodyAlias(): ResponseBody? { 8 | return this.body 9 | } 10 | 11 | fun Response.codeAlias(): Int { 12 | return this.code 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/SP.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import android.util.Log 7 | import com.lizongying.mytv0.data.Global.gson 8 | import com.lizongying.mytv0.data.Global.typeSourceList 9 | import com.lizongying.mytv0.data.Source 10 | import io.github.lizongying.Gua 11 | 12 | object SP { 13 | private const val TAG = "SP" 14 | 15 | // If Change channel with up and down in reversed order or not 16 | private const val KEY_CHANNEL_REVERSAL = "channel_reversal" 17 | 18 | // If use channel num to select channel or not 19 | private const val KEY_CHANNEL_NUM = "channel_num" 20 | 21 | private const val KEY_TIME = "time" 22 | 23 | // If start app on device boot or not 24 | private const val KEY_BOOT_STARTUP = "boot_startup" 25 | 26 | // Position in list of the selected channel item 27 | private const val KEY_POSITION = "position" 28 | 29 | private const val KEY_POSITION_GROUP = "position_group" 30 | 31 | private const val KEY_POSITION_SUB = "position_sub" 32 | 33 | private const val KEY_REPEAT_INFO = "repeat_info" 34 | 35 | private const val KEY_CONFIG_URL = "config" 36 | 37 | private const val KEY_CONFIG_AUTO_LOAD = "config_auto_load" 38 | 39 | private const val KEY_CHANNEL = "channel" 40 | 41 | private const val KEY_DEFAULT_LIKE = "default_like" 42 | 43 | private const val KEY_DISPLAY_SECONDS = "display_seconds" 44 | 45 | private const val KEY_SHOW_ALL_CHANNELS = "show_all_channels" 46 | 47 | private const val KEY_COMPACT_MENU = "compact_menu" 48 | 49 | private const val KEY_LIKE = "like" 50 | 51 | private const val KEY_PROXY = "proxy" 52 | 53 | private const val KEY_EPG = "epg" 54 | 55 | private const val KEY_VERSION = "version" 56 | 57 | private const val KEY_LOG_TIMES = "log_times" 58 | 59 | private const val KEY_SOURCES = "sources" 60 | 61 | private const val KEY_SOFT_DECODE = "soft_decode" 62 | 63 | const val DEFAULT_CHANNEL_REVERSAL = false 64 | const val DEFAULT_CHANNEL_NUM = false 65 | const val DEFAULT_TIME = true 66 | const val DEFAULT_BOOT_STARTUP = false 67 | const val DEFAULT_CONFIG_URL = "" 68 | const val DEFAULT_PROXY = "" 69 | const val DEFAULT_EPG = 70 | "https://live.fanmingming.cn/e.xml,https://raw.githubusercontent.com/fanmingming/live/main/e.xml" 71 | const val DEFAULT_CHANNEL = 0 72 | const val DEFAULT_SHOW_ALL_CHANNELS = false 73 | const val DEFAULT_COMPACT_MENU = true 74 | const val DEFAULT_DISPLAY_SECONDS = true 75 | const val DEFAULT_LOG_TIMES = 10 76 | const val DEFAULT_SOFT_DECODE = false 77 | 78 | // 0 favorite; 1 all 79 | const val DEFAULT_POSITION_GROUP = 1 80 | const val DEFAULT_POSITION = 0 81 | const val DEFAULT_REPEAT_INFO = true 82 | const val DEFAULT_CONFIG_AUTO_LOAD = false 83 | var DEFAULT_SOURCES = "" 84 | 85 | private lateinit var sp: SharedPreferences 86 | 87 | /** 88 | * The method must be invoked as early as possible(At least before using the keys) 89 | */ 90 | fun init(context: Context) { 91 | sp = context.getSharedPreferences( 92 | context.getString(R.string.app_name), 93 | Context.MODE_PRIVATE 94 | ) 95 | 96 | context.resources.openRawResource(R.raw.sources).bufferedReader() 97 | .use { 98 | val str = it.readText() 99 | if (str.isNotEmpty()) { 100 | DEFAULT_SOURCES = gson.toJson( 101 | Gua().decode(str).trim().split("\n").map { i -> 102 | Source( 103 | uri = i 104 | ) 105 | }, typeSourceList 106 | ) ?: "" 107 | } 108 | } 109 | 110 | Log.i(TAG, "group position $positionGroup") 111 | Log.i(TAG, "list position $position") 112 | Log.i(TAG, "default channel $channel") 113 | Log.i(TAG, "proxy $proxy") 114 | } 115 | 116 | var channelReversal: Boolean 117 | get() = sp.getBoolean(KEY_CHANNEL_REVERSAL, DEFAULT_CHANNEL_REVERSAL) 118 | set(value) = sp.edit().putBoolean(KEY_CHANNEL_REVERSAL, value).apply() 119 | 120 | var channelNum: Boolean 121 | get() = sp.getBoolean(KEY_CHANNEL_NUM, DEFAULT_CHANNEL_NUM) 122 | set(value) = sp.edit().putBoolean(KEY_CHANNEL_NUM, value).apply() 123 | 124 | var time: Boolean 125 | get() = sp.getBoolean(KEY_TIME, DEFAULT_TIME) 126 | set(value) = sp.edit().putBoolean(KEY_TIME, value).apply() 127 | 128 | var bootStartup: Boolean 129 | get() = sp.getBoolean(KEY_BOOT_STARTUP, DEFAULT_BOOT_STARTUP) 130 | set(value) = sp.edit().putBoolean(KEY_BOOT_STARTUP, value).apply() 131 | 132 | var positionGroup: Int 133 | get() = sp.getInt(KEY_POSITION_GROUP, DEFAULT_POSITION_GROUP) 134 | set(value) = sp.edit().putInt(KEY_POSITION_GROUP, value).apply() 135 | 136 | var position: Int 137 | get() = sp.getInt(KEY_POSITION, DEFAULT_POSITION) 138 | set(value) = sp.edit().putInt(KEY_POSITION, value).apply() 139 | 140 | var positionSub: Int 141 | get() = sp.getInt(KEY_POSITION_SUB, 0) 142 | set(value) = sp.edit().putInt(KEY_POSITION_SUB, value).apply() 143 | 144 | var repeatInfo: Boolean 145 | get() = sp.getBoolean(KEY_REPEAT_INFO, DEFAULT_REPEAT_INFO) 146 | set(value) = sp.edit().putBoolean(KEY_REPEAT_INFO, value).apply() 147 | 148 | var configUrl: String? 149 | get() = sp.getString(KEY_CONFIG_URL, DEFAULT_CONFIG_URL) 150 | set(value) = sp.edit().putString(KEY_CONFIG_URL, value).apply() 151 | 152 | var configAutoLoad: Boolean 153 | get() = sp.getBoolean(KEY_CONFIG_AUTO_LOAD, DEFAULT_CONFIG_AUTO_LOAD) 154 | set(value) = sp.edit().putBoolean(KEY_CONFIG_AUTO_LOAD, value).apply() 155 | 156 | var channel: Int 157 | get() = sp.getInt(KEY_CHANNEL, DEFAULT_CHANNEL) 158 | set(value) = sp.edit().putInt(KEY_CHANNEL, value).apply() 159 | 160 | var compactMenu: Boolean 161 | get() = sp.getBoolean(KEY_COMPACT_MENU, DEFAULT_COMPACT_MENU) 162 | set(value) = sp.edit().putBoolean(KEY_COMPACT_MENU, value).apply() 163 | 164 | var showAllChannels: Boolean 165 | get() = sp.getBoolean(KEY_SHOW_ALL_CHANNELS, DEFAULT_SHOW_ALL_CHANNELS) 166 | set(value) = sp.edit().putBoolean(KEY_SHOW_ALL_CHANNELS, value).apply() 167 | 168 | var defaultLike: Boolean 169 | get() = sp.getBoolean(KEY_DEFAULT_LIKE, false) 170 | set(value) = sp.edit().putBoolean(KEY_DEFAULT_LIKE, value).apply() 171 | 172 | var displaySeconds: Boolean 173 | get() = sp.getBoolean(KEY_DISPLAY_SECONDS, DEFAULT_DISPLAY_SECONDS) 174 | set(value) = sp.edit().putBoolean(KEY_DISPLAY_SECONDS, value).apply() 175 | 176 | var softDecode: Boolean 177 | get() = sp.getBoolean(KEY_SOFT_DECODE, DEFAULT_SOFT_DECODE) 178 | set(value) = sp.edit().putBoolean(KEY_SOFT_DECODE, value).apply() 179 | 180 | fun getLike(id: Int): Boolean { 181 | val stringSet = sp.getStringSet(KEY_LIKE, emptySet()) 182 | return stringSet?.contains(id.toString()) ?: false 183 | } 184 | 185 | fun setLike(id: Int, liked: Boolean) { 186 | val stringSet = sp.getStringSet(KEY_LIKE, emptySet())?.toMutableSet() ?: mutableSetOf() 187 | if (liked) { 188 | stringSet.add(id.toString()) 189 | } else { 190 | stringSet.remove(id.toString()) 191 | } 192 | 193 | sp.edit().putStringSet(KEY_LIKE, stringSet).apply() 194 | } 195 | 196 | fun deleteLike() { 197 | sp.edit().remove(KEY_LIKE).apply() 198 | } 199 | 200 | var proxy: String? 201 | get() = sp.getString(KEY_PROXY, DEFAULT_PROXY) 202 | set(value) = sp.edit().putString(KEY_PROXY, value).apply() 203 | 204 | var epg: String? 205 | get() = sp.getString(KEY_EPG, DEFAULT_EPG) 206 | set(value) = sp.edit().putString(KEY_EPG, value).apply() 207 | 208 | var version: String? 209 | get() = sp.getString(KEY_VERSION, "") 210 | set(value) = sp.edit().putString(KEY_VERSION, value).apply() 211 | 212 | var logTimes: Int 213 | get() = sp.getInt(KEY_LOG_TIMES, DEFAULT_LOG_TIMES) 214 | set(value) = sp.edit().putInt(KEY_LOG_TIMES, value).apply() 215 | 216 | var sources: String? 217 | get() = sp.getString(KEY_SOURCES, DEFAULT_SOURCES) 218 | set(value) = sp.edit().putString(KEY_SOURCES, value).apply() 219 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/SourcesAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import android.content.Context 4 | import android.view.KeyEvent 5 | import android.view.LayoutInflater 6 | import android.view.MotionEvent 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.core.content.ContextCompat 10 | import androidx.core.view.setPadding 11 | import androidx.recyclerview.widget.LinearLayoutManager 12 | import androidx.recyclerview.widget.RecyclerView 13 | import com.lizongying.mytv0.databinding.SourcesItemBinding 14 | import com.lizongying.mytv0.models.Sources 15 | import java.util.Locale 16 | 17 | 18 | class SourcesAdapter( 19 | private val context: Context, 20 | private val recyclerView: RecyclerView, 21 | private var sources: Sources, 22 | ) : 23 | RecyclerView.Adapter() { 24 | private var listener: ItemListener? = null 25 | 26 | val application = context.applicationContext as MyTVApplication 27 | 28 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 29 | val inflater = LayoutInflater.from(context) 30 | val binding = SourcesItemBinding.inflate(inflater, parent, false) 31 | 32 | binding.num.layoutParams.width = application.px2Px(binding.num.layoutParams.width) 33 | binding.num.layoutParams.height = application.px2Px(binding.num.layoutParams.height) 34 | binding.num.textSize = application.px2PxFont(binding.num.textSize) 35 | 36 | binding.title.layoutParams.width = application.px2Px(binding.title.layoutParams.width) 37 | binding.title.layoutParams.height = application.px2Px(binding.title.layoutParams.height) 38 | binding.title.textSize = application.px2PxFont(binding.title.textSize) 39 | 40 | binding.heart.layoutParams.width = application.px2Px(binding.heart.layoutParams.width) 41 | binding.heart.layoutParams.height = application.px2Px(binding.heart.layoutParams.height) 42 | binding.heart.setPadding(application.px2Px(binding.heart.paddingTop)) 43 | 44 | return ViewHolder(context, binding) 45 | } 46 | 47 | override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { 48 | sources.let { 49 | val source = it.getSource(position)!! 50 | 51 | val view = viewHolder.itemView 52 | view.isFocusable = true 53 | view.isFocusableInTouchMode = true 54 | 55 | viewHolder.checked(source.checked) 56 | 57 | view.setOnFocusChangeListener { _, hasFocus -> 58 | listener?.onItemFocusChange(position, hasFocus) 59 | 60 | viewHolder.focus(hasFocus) 61 | } 62 | 63 | view.setOnClickListener { _ -> 64 | it.setChecked(position) 65 | // ui 66 | check(position) 67 | listener?.onItemClicked(position) 68 | } 69 | 70 | view.setOnTouchListener(object : View.OnTouchListener { 71 | override fun onTouch( 72 | v: View?, 73 | event: MotionEvent? 74 | ): Boolean { 75 | v ?: return false 76 | event ?: return false 77 | 78 | when (event.action) { 79 | MotionEvent.ACTION_UP -> { 80 | v.performClick() 81 | return true 82 | } 83 | } 84 | 85 | return false 86 | } 87 | }) 88 | 89 | view.setOnKeyListener { _, keyCode, event: KeyEvent? -> 90 | if (event?.action == KeyEvent.ACTION_DOWN) { 91 | if (keyCode == KeyEvent.KEYCODE_DPAD_UP && position == 0) { 92 | val p = getItemCount() - 1 93 | 94 | (recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset( 95 | p, 96 | 0 97 | ) 98 | 99 | recyclerView.postDelayed({ 100 | val v = recyclerView.findViewHolderForAdapterPosition(p) 101 | v?.itemView?.isSelected = true 102 | v?.itemView?.requestFocus() 103 | }, 0) 104 | } 105 | 106 | if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && position == getItemCount() - 1) { 107 | val p = 0 108 | 109 | (recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset( 110 | p, 111 | 0 112 | ) 113 | 114 | recyclerView.postDelayed({ 115 | val v = recyclerView.findViewHolderForAdapterPosition(p) 116 | v?.itemView?.isSelected = true 117 | v?.itemView?.requestFocus() 118 | }, 0) 119 | } 120 | 121 | return@setOnKeyListener listener?.onKey(keyCode) ?: false 122 | } 123 | false 124 | } 125 | 126 | viewHolder.bindNum("%02d".format(position)) 127 | viewHolder.bindTitle(source.uri) 128 | } 129 | } 130 | 131 | override fun getItemCount() = sources.size() 132 | 133 | class ViewHolder(private val context: Context, val binding: SourcesItemBinding) : 134 | RecyclerView.ViewHolder(binding.root) { 135 | fun bindNum(text: String) { 136 | binding.num.text = text 137 | } 138 | 139 | fun bindTitle(text: String) { 140 | binding.title.text = text 141 | } 142 | 143 | fun focus(hasFocus: Boolean) { 144 | if (hasFocus) { 145 | binding.title.setTextColor(ContextCompat.getColor(context, R.color.white)) 146 | binding.root.setBackgroundResource(R.color.focus) 147 | } else { 148 | binding.title.setTextColor(ContextCompat.getColor(context, R.color.title_blur)) 149 | binding.root.setBackgroundResource(R.color.blur) 150 | } 151 | } 152 | 153 | // show done icon 154 | fun checked(isChecked: Boolean) { 155 | binding.heart.visibility = if (isChecked) View.VISIBLE else View.GONE 156 | } 157 | } 158 | 159 | private fun check(position: Int) { 160 | recyclerView.post { 161 | for (i in 0 until getItemCount()) { 162 | val changed = this.sources.setSourceChecked(i, i == position) 163 | if (changed) { 164 | notifyItemChanged(i) 165 | } 166 | } 167 | } 168 | } 169 | 170 | fun removed(position: Int) { 171 | recyclerView.post { 172 | notifyItemRemoved(position) 173 | } 174 | } 175 | 176 | fun added(position: Int) { 177 | recyclerView.post { 178 | notifyItemInserted(position) 179 | } 180 | } 181 | 182 | fun changed() { 183 | recyclerView.post { 184 | notifyDataSetChanged() 185 | } 186 | } 187 | 188 | fun toPosition(position: Int) { 189 | recyclerView.post { 190 | (recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset( 191 | position, 192 | 0 193 | ) 194 | recyclerView.postDelayed({ 195 | val viewHolder = recyclerView.findViewHolderForAdapterPosition(position) 196 | viewHolder?.itemView?.isSelected = true 197 | viewHolder?.itemView?.requestFocus() 198 | }, 0) 199 | } 200 | } 201 | 202 | interface ItemListener { 203 | fun onItemFocusChange(position: Int, hasFocus: Boolean, tag: String = TAG) 204 | fun onItemClicked(position: Int, tag: String = TAG) 205 | fun onKey(keyCode: Int, tag: String = TAG): Boolean 206 | } 207 | 208 | fun setItemListener(listener: ItemListener) { 209 | this.listener = listener 210 | } 211 | 212 | companion object { 213 | private const val TAG = "SourcesAdapter" 214 | } 215 | } 216 | 217 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/SourcesFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import MainViewModel 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import android.os.Handler 7 | import android.os.Looper 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import android.view.WindowManager 12 | import androidx.fragment.app.DialogFragment 13 | import androidx.lifecycle.ViewModelProvider 14 | import androidx.recyclerview.widget.LinearLayoutManager 15 | import com.lizongying.mytv0.databinding.SourcesBinding 16 | 17 | 18 | class SourcesFragment : DialogFragment(), SourcesAdapter.ItemListener { 19 | private var _binding: SourcesBinding? = null 20 | private val binding get() = _binding!! 21 | 22 | private val handler = Handler(Looper.myLooper()!!) 23 | private val delayHideFragment = 10000L 24 | 25 | private lateinit var viewModel: MainViewModel 26 | 27 | override fun onStart() { 28 | super.onStart() 29 | dialog?.window?.apply { 30 | addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) 31 | decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 32 | } 33 | } 34 | 35 | override fun onCreateView( 36 | inflater: LayoutInflater, 37 | container: ViewGroup?, 38 | savedInstanceState: Bundle? 39 | ): View { 40 | _binding = SourcesBinding.inflate(inflater, container, false) 41 | return binding.root 42 | } 43 | 44 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 45 | super.onViewCreated(view, savedInstanceState) 46 | 47 | viewModel = ViewModelProvider(requireActivity())[MainViewModel::class.java] 48 | 49 | val context = requireActivity() 50 | val application = context.applicationContext as MyTVApplication 51 | val sourcesAdapter = SourcesAdapter( 52 | context, 53 | binding.list, 54 | viewModel.sources, 55 | ) 56 | binding.list.adapter = sourcesAdapter 57 | binding.list.layoutManager = 58 | LinearLayoutManager(context) 59 | val listWidth = application.px2Px(binding.list.layoutParams.width) 60 | binding.list.layoutParams.width = listWidth 61 | sourcesAdapter.setItemListener(this) 62 | sourcesAdapter.toPosition(if (viewModel.sources.checkedValue > -1) viewModel.sources.checkedValue else 0) 63 | 64 | handler.postDelayed(hideFragment, delayHideFragment) 65 | 66 | viewModel.sources.removed.observe(this) { items -> 67 | sourcesAdapter.removed(items.first) 68 | checkEmpty() 69 | } 70 | 71 | viewModel.sources.added.observe(this) { items -> 72 | sourcesAdapter.added(items.first) 73 | } 74 | 75 | viewModel.sources.changed.observe(this) { _ -> 76 | sourcesAdapter.changed() 77 | checkEmpty() 78 | } 79 | 80 | checkEmpty() 81 | } 82 | 83 | private val hideFragment = Runnable { 84 | if (!this.isHidden) { 85 | this.dismiss() 86 | } 87 | } 88 | 89 | override fun onDestroyView() { 90 | super.onDestroyView() 91 | _binding = null 92 | handler.removeCallbacksAndMessages(null) 93 | } 94 | 95 | override fun onItemFocusChange(position: Int, hasFocus: Boolean, tag: String) { 96 | } 97 | 98 | override fun onItemClicked(position: Int, tag: String) { 99 | viewModel.sources.getSource(position)?.let { 100 | val uri = Uri.parse(it.uri) 101 | handler.post { 102 | viewModel.importFromUri(uri) 103 | } 104 | } 105 | 106 | handler.postDelayed(hideFragment, 0) 107 | } 108 | 109 | override fun onKey(keyCode: Int, tag: String): Boolean { 110 | handler.removeCallbacks(hideFragment) 111 | handler.postDelayed(hideFragment, delayHideFragment) 112 | return false 113 | } 114 | 115 | private fun checkEmpty() { 116 | if (viewModel.sources.size() == 0) { 117 | binding.content.visibility = View.VISIBLE 118 | } else { 119 | binding.content.visibility = View.GONE 120 | } 121 | } 122 | 123 | companion object { 124 | const val TAG = "SourcesFragment" 125 | } 126 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/TimeFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import MainViewModel 4 | import android.os.Bundle 5 | import android.util.Log 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.core.view.marginEnd 10 | import androidx.core.view.marginTop 11 | import androidx.fragment.app.Fragment 12 | import androidx.lifecycle.ViewModelProvider 13 | import androidx.lifecycle.lifecycleScope 14 | import com.lizongying.mytv0.databinding.TimeBinding 15 | import kotlinx.coroutines.Job 16 | import kotlinx.coroutines.delay 17 | import kotlinx.coroutines.isActive 18 | import kotlinx.coroutines.launch 19 | 20 | class TimeFragment : Fragment() { 21 | private var _binding: TimeBinding? = null 22 | private val binding get() = _binding!! 23 | 24 | private val delay: Long = 1000 25 | 26 | private var job: Job? = null 27 | 28 | private lateinit var viewModel: MainViewModel 29 | 30 | override fun onCreateView( 31 | inflater: LayoutInflater, container: ViewGroup?, 32 | savedInstanceState: Bundle? 33 | ): View { 34 | _binding = TimeBinding.inflate(inflater, container, false) 35 | 36 | val application = requireActivity().applicationContext as MyTVApplication 37 | 38 | binding.time.layoutParams.width = application.px2Px(binding.time.layoutParams.width) 39 | binding.time.layoutParams.height = application.px2Px(binding.time.layoutParams.height) 40 | 41 | val layoutParams = binding.time.layoutParams as ViewGroup.MarginLayoutParams 42 | layoutParams.topMargin = application.px2Px(binding.time.marginTop) 43 | layoutParams.marginEnd = application.px2Px(binding.time.marginEnd) 44 | binding.time.layoutParams = layoutParams 45 | 46 | binding.content.textSize = application.px2PxFont(binding.content.textSize) 47 | binding.channel.textSize = application.px2PxFont(binding.channel.textSize) 48 | 49 | binding.main.layoutParams.width = application.shouldWidthPx() 50 | binding.main.layoutParams.height = application.shouldHeightPx() 51 | 52 | return binding.root 53 | } 54 | 55 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 56 | super.onViewCreated(view, savedInstanceState) 57 | viewModel = ViewModelProvider(requireActivity())[MainViewModel::class.java] 58 | 59 | job = viewLifecycleOwner.lifecycleScope.launch { 60 | while (isActive) { 61 | binding.content.text = viewModel.getTime() 62 | delay(delay) 63 | } 64 | } 65 | } 66 | 67 | override fun onHiddenChanged(hidden: Boolean) { 68 | super.onHiddenChanged(hidden) 69 | if (!hidden) { 70 | if (_binding == null) { 71 | Log.w(TAG, "_binding is null") 72 | return 73 | } 74 | 75 | if (!this::viewModel.isInitialized) { 76 | Log.w(TAG, "viewModel is not initialized") 77 | return 78 | } 79 | 80 | job = viewLifecycleOwner.lifecycleScope.launch { 81 | while (isActive) { 82 | binding.content.text = viewModel.getTime() 83 | delay(delay) 84 | } 85 | } 86 | } else { 87 | job?.cancel() 88 | job = null 89 | } 90 | } 91 | 92 | override fun onDestroyView() { 93 | super.onDestroyView() 94 | _binding = null 95 | job?.cancel() 96 | job = null 97 | } 98 | 99 | companion object { 100 | private const val TAG = "TimeFragment" 101 | } 102 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0 2 | 3 | import android.content.res.Resources 4 | import android.util.Log 5 | import android.util.TypedValue 6 | import androidx.lifecycle.LiveData 7 | import androidx.lifecycle.MutableLiveData 8 | import com.lizongying.mytv0.ISP.CHINA_MOBILE 9 | import com.lizongying.mytv0.ISP.CHINA_TELECOM 10 | import com.lizongying.mytv0.ISP.CHINA_UNICOM 11 | import com.lizongying.mytv0.ISP.UNKNOWN 12 | import com.lizongying.mytv0.data.Global.gson 13 | import com.lizongying.mytv0.requests.HttpClient 14 | import kotlinx.coroutines.CoroutineScope 15 | import kotlinx.coroutines.Dispatchers 16 | import kotlinx.coroutines.launch 17 | import kotlinx.coroutines.withContext 18 | import java.text.SimpleDateFormat 19 | import java.util.Date 20 | import java.util.Locale 21 | 22 | enum class ISP { 23 | UNKNOWN, 24 | CHINA_MOBILE, 25 | CHINA_UNICOM, 26 | CHINA_TELECOM, 27 | IPV6, 28 | } 29 | 30 | data class IpInfo( 31 | val ip: String, 32 | val location: Location 33 | ) 34 | 35 | data class Location( 36 | val city_name: String, 37 | val country_name: String, 38 | val isp_domain: String, 39 | val latitude: String, 40 | val longitude: String, 41 | val owner_domain: String, 42 | val region_name: String, 43 | ) 44 | 45 | 46 | object Utils { 47 | const val TAG = "Utils" 48 | 49 | private var between: Long = 0 50 | 51 | private val _isp = MutableLiveData() 52 | val isp: LiveData 53 | get() = _isp 54 | 55 | fun getDateFormat(format: String): String { 56 | return SimpleDateFormat( 57 | format, 58 | Locale.CHINA 59 | ).format(Date(System.currentTimeMillis() - between)) 60 | } 61 | 62 | fun getDateFormat(format: String, seconds: Int): String { 63 | return SimpleDateFormat( 64 | format, 65 | Locale.CHINA 66 | ).format(Date(seconds * 1000L)) 67 | } 68 | 69 | fun getDateTimestamp(): Long { 70 | return (System.currentTimeMillis() - between) / 1000 71 | } 72 | 73 | init { 74 | CoroutineScope(Dispatchers.IO).launch { 75 | try { 76 | val currentTimeMillis = getTimestampFromServer() 77 | Log.i(TAG, "currentTimeMillis $currentTimeMillis") 78 | if (currentTimeMillis > 0) { 79 | between = System.currentTimeMillis() - currentTimeMillis 80 | } 81 | } catch (e: Exception) { 82 | Log.e(TAG, "init", e) 83 | } 84 | 85 | // try { 86 | // withContext(Dispatchers.Main) { 87 | // _isp.value = getISP() 88 | // } 89 | // } catch (e: Exception) { 90 | // e.printStackTrace() 91 | // } 92 | } 93 | } 94 | 95 | private suspend fun getTimestampFromServer(): Long { 96 | return withContext(Dispatchers.IO) { 97 | try { 98 | val request = okhttp3.Request.Builder() 99 | .url("https://ip.ddnspod.com/timestamp") 100 | .build() 101 | 102 | HttpClient.okHttpClient.newCall(request).execute().use { response -> 103 | if (!response.isSuccessful) return@withContext 0 104 | response.bodyAlias()?.string()?.toLong() ?: 0 105 | } 106 | } catch (e: Exception) { 107 | Log.e(TAG, "getTimestampFromServer", e) 108 | 0 109 | } 110 | } 111 | } 112 | 113 | private suspend fun getISP(): ISP { 114 | return withContext(Dispatchers.IO) { 115 | try { 116 | val request = okhttp3.Request.Builder() 117 | .url("https://api.myip.la/json") 118 | .build() 119 | 120 | HttpClient.okHttpClient.newCall(request).execute().use { response -> 121 | if (!response.isSuccessful) return@withContext UNKNOWN 122 | val string = response.bodyAlias()?.string() 123 | val isp = gson.fromJson(string, IpInfo::class.java).location.isp_domain 124 | when (isp) { 125 | "ChinaMobile" -> CHINA_MOBILE 126 | "ChinaUnicom" -> CHINA_UNICOM 127 | "ChinaTelecom" -> CHINA_TELECOM 128 | else -> UNKNOWN 129 | } 130 | } 131 | } catch (e: Exception) { 132 | Log.e(TAG, "getISP", e) 133 | UNKNOWN 134 | } 135 | } 136 | } 137 | 138 | fun dpToPx(dp: Float): Int { 139 | return TypedValue.applyDimension( 140 | TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().displayMetrics 141 | ).toInt() 142 | } 143 | 144 | fun dpToPx(dp: Int): Int { 145 | return TypedValue.applyDimension( 146 | TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), Resources.getSystem().displayMetrics 147 | ).toInt() 148 | } 149 | 150 | fun formatUrl(url: String): String { 151 | if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://") || url.startsWith( 152 | "socks://" 153 | ) || url.startsWith("socks5://") 154 | ) { 155 | return url 156 | } 157 | 158 | if (url.startsWith("//")) { 159 | return "http:$url" 160 | } 161 | 162 | return "http://$url" 163 | } 164 | 165 | fun getUrls(url: String): List { 166 | return if (url.startsWith("https://raw.githubusercontent.com") || url.startsWith("https://github.com")) { 167 | listOf( 168 | "https://gh.llkk.cc/", 169 | "https://github.moeyy.xyz/", 170 | "https://mirror.ghproxy.com/", 171 | "https://ghproxy.cn/", 172 | "https://ghproxy.net/", 173 | "https://ghproxy.click/", 174 | "https://ghproxy.com/", 175 | "https://github.moeyy.cn/", 176 | "https://gh-proxy.llyke.com/", 177 | "https://www.ghproxy.cc/", 178 | "https://cf.ghproxy.cc/", 179 | "https://ghp.ci/", 180 | "https://ghfast.top" 181 | ).map { 182 | "$it$url" 183 | } 184 | } else { 185 | listOf(url) 186 | } 187 | } 188 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/data/EPG.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0.data 2 | 3 | 4 | data class EPG( 5 | val title: String, 6 | val beginTime: Int, 7 | val endTime: Int, 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/data/Global.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0.data 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.reflect.TypeToken 5 | 6 | object Global { 7 | val gson = Gson() 8 | 9 | val typeTvList = object : TypeToken>() {}.type 10 | 11 | val typeSourceList = object : TypeToken>() {}.type 12 | 13 | val typeEPGMap = object : TypeToken>>() {}.type 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/data/ReleaseResponse.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0.data 2 | 3 | 4 | data class ReleaseResponse( 5 | val version_code: Int?, 6 | val version_name: String?, 7 | val apk_name: String?, 8 | val apk_url: String?, 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/data/ReqSettings.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0.data 2 | 3 | data class ReqSettings( 4 | var uri: String? = "", 5 | val proxy: String?, 6 | val epg: String?, 7 | val channel: Int?, 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/data/ReqSources.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0.data 2 | 3 | data class ReqSources( 4 | var sourceId: String, 5 | ) 6 | 7 | data class ReqSourceAdd( 8 | val id: String, 9 | var uri: String, 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/data/RespSettings.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0.data 2 | 3 | data class RespSettings( 4 | val channelUri: String, 5 | val channelText: String, 6 | val channelDefault: Int, 7 | val proxy: String, 8 | val epg: String, 9 | val history: List, 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/data/Source.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0.data 2 | 3 | import java.util.UUID 4 | 5 | data class Source( 6 | var id: String? = null, 7 | var uri: String, 8 | var checked: Boolean = false, 9 | ) { 10 | init { 11 | if (id.isNullOrEmpty()) { 12 | id = UUID.randomUUID().toString() 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/data/SourceType.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0.data 2 | 3 | enum class SourceType { 4 | UNKNOWN, 5 | HLS, 6 | DASH, 7 | RTSP, 8 | RTMP, 9 | RTP, 10 | PROGRESSIVE, 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/data/TV.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0.data 2 | 3 | import java.io.Serializable 4 | 5 | data class TV( 6 | var id: Int = -1, 7 | var name: String = "", 8 | var title: String = "", 9 | var description: String? = null, 10 | var logo: String = "", 11 | var image: String? = null, 12 | var uris: List = emptyList(), 13 | var videoIndex: Int = 0, 14 | var headers: Map? = null, 15 | var group: String = "", 16 | var sourceType: SourceType = SourceType.UNKNOWN, 17 | var number: Int = -1, 18 | var child: List = emptyList(), 19 | ) : Serializable { 20 | 21 | override fun toString(): String { 22 | return "TV{" + 23 | "id=" + id + 24 | ", name='" + name + '\'' + 25 | ", title='" + title + '\'' + 26 | ", description='" + description + '\'' + 27 | ", logo='" + logo + '\'' + 28 | ", image='" + image + '\'' + 29 | ", uris=" + uris + 30 | ", headers=" + headers + 31 | ", group='" + group + '\'' + 32 | ", sourceType='" + sourceType + '\'' + 33 | ", number=" + number + 34 | '}' 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/models/EPGXmlParser.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0.models 2 | 3 | import android.util.Xml 4 | import com.lizongying.mytv0.Utils.getDateTimestamp 5 | import com.lizongying.mytv0.data.EPG 6 | import org.xmlpull.v1.XmlPullParser 7 | import java.io.InputStream 8 | import java.text.SimpleDateFormat 9 | import java.util.Locale 10 | 11 | 12 | class EPGXmlParser { 13 | 14 | private val ns: String? = null 15 | private val epg = mutableMapOf>() 16 | private val dateFormat = SimpleDateFormat("yyyyMMddHHmmss Z", Locale.getDefault()) 17 | private val now = getDateTimestamp() 18 | 19 | private fun formatFTime(s: String): Int { 20 | return dateFormat.parse(s)?.time?.div(1000)?.toInt() ?: 0 21 | } 22 | 23 | fun parse(inputStream: InputStream): Map> { 24 | inputStream.use { input -> 25 | val parser: XmlPullParser = Xml.newPullParser() 26 | parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false) 27 | parser.setInput(input, null) 28 | parser.nextTag() 29 | var channel = "" 30 | while (parser.eventType != XmlPullParser.END_DOCUMENT) { 31 | if (parser.eventType != XmlPullParser.START_TAG) { 32 | parser.next() 33 | continue 34 | } 35 | if (parser.name == CHANNEL_TAG) { 36 | parser.nextTag() 37 | channel = parser.nextText() 38 | epg[channel] = mutableListOf() 39 | } else if (parser.name == PROGRAMME_TAG) { 40 | val start = parser.getAttributeValue(ns, START_ATTRIBUTE) 41 | val stop = parser.getAttributeValue(ns, STOP_ATTRIBUTE) 42 | parser.nextTag() 43 | val title = parser.nextText() 44 | if (formatFTime(stop) > now) { 45 | epg[channel]?.add(EPG(title, formatFTime(start), formatFTime(stop))) 46 | } 47 | } 48 | parser.next() 49 | } 50 | } 51 | 52 | return epg.toSortedMap { a, b -> b.compareTo(a) } 53 | } 54 | 55 | companion object { 56 | private const val CHANNEL_TAG = "channel" 57 | private const val PROGRAMME_TAG = "programme" 58 | private const val START_ATTRIBUTE = "start" 59 | private const val STOP_ATTRIBUTE = "stop" 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/models/Sources.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0.models 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import com.lizongying.mytv0.SP 7 | import com.lizongying.mytv0.data.Global.gson 8 | import com.lizongying.mytv0.data.Global.typeSourceList 9 | import com.lizongying.mytv0.data.Source 10 | 11 | class Sources { 12 | var version = 0 13 | 14 | private val _removed = MutableLiveData>() 15 | val removed: LiveData> 16 | get() = _removed 17 | 18 | private val _added = MutableLiveData>() 19 | val added: LiveData> 20 | get() = _added 21 | 22 | private val _changed = MutableLiveData() 23 | val changed: LiveData 24 | get() = _changed 25 | 26 | private val _sources = MutableLiveData>() 27 | val sources: LiveData> 28 | get() = _sources 29 | private val sourcesValue: List 30 | get() = _sources.value ?: emptyList() 31 | 32 | private val _checked = MutableLiveData() 33 | val checked: LiveData 34 | get() = _checked 35 | val checkedValue: Int 36 | get() = _checked.value ?: DEFAULT_CHECKED 37 | 38 | fun setChecked(position: Int) { 39 | _checked.value = position 40 | } 41 | 42 | fun setSourceChecked(position: Int, checked: Boolean): Boolean { 43 | val checkedBefore = getSource(position)?.checked 44 | if (checkedBefore == checked) { 45 | return false 46 | } else { 47 | getSource(position)?.checked = checked 48 | // if (checked) { 49 | // Log.i(TAG, "setChecked $position") 50 | // setChecked(position) 51 | // } 52 | // 53 | // SP.sources = gson.toJson(sources, type) ?: "" 54 | return true 55 | } 56 | } 57 | 58 | private fun setSources(sources: List) { 59 | _sources.value = sources 60 | SP.sources = gson.toJson(sources, typeSourceList) ?: "" 61 | } 62 | 63 | fun addSource(source: Source) { 64 | val index = sourcesValue.indexOfFirst { it.uri == source.uri } 65 | if (index == -1) { 66 | setSourceChecked(checkedValue, false) 67 | 68 | _sources.value = sourcesValue.toMutableList().apply { 69 | add(0, source) 70 | } 71 | 72 | _checked.value = 0 73 | setSourceChecked(checkedValue, true) 74 | SP.sources = gson.toJson(sourcesValue, typeSourceList) ?: "" 75 | 76 | _changed.value = version 77 | version++ 78 | } 79 | } 80 | 81 | fun removeSource(id: String): Boolean { 82 | if (sourcesValue.isEmpty()) { 83 | Log.i(TAG, "sources is empty") 84 | return false 85 | } 86 | 87 | val index = sourcesValue.indexOfFirst { it.id == id } 88 | if (index != -1) { 89 | _sources.value = sourcesValue.toMutableList().apply { 90 | removeAt(index) 91 | } 92 | SP.sources = gson.toJson(sourcesValue, typeSourceList) ?: "" 93 | 94 | _removed.value = Pair(index, version) 95 | version++ 96 | return true 97 | } 98 | 99 | Log.i(TAG, "sourceId is not exists") 100 | return false 101 | } 102 | 103 | fun getSource(idx: Int): Source? { 104 | if (idx < 0 || idx >= size()) { 105 | return null 106 | } 107 | 108 | return sourcesValue[idx] 109 | } 110 | 111 | fun init() { 112 | if (!SP.sources.isNullOrEmpty()) { 113 | try { 114 | val sources: List = gson.fromJson(SP.sources!!, typeSourceList) 115 | setSources(sources.map { it.apply { checked = false } }) 116 | } catch (e: Exception) { 117 | e.printStackTrace() 118 | SP.sources = SP.DEFAULT_SOURCES 119 | } 120 | } 121 | 122 | if (size() > 0) { 123 | _checked.value = sourcesValue.indexOfFirst { it.uri == SP.configUrl } 124 | 125 | if (checkedValue > -1) { 126 | setSourceChecked(checkedValue, true) 127 | } 128 | } 129 | 130 | _changed.value = version 131 | version++ 132 | } 133 | 134 | init { 135 | init() 136 | } 137 | 138 | fun size(): Int { 139 | return sourcesValue.size 140 | } 141 | 142 | companion object { 143 | const val TAG = "Sources" 144 | const val DEFAULT_CHECKED = -1 145 | } 146 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/models/TVGroupModel.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0.models 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import com.lizongying.mytv0.SP 8 | 9 | class TVGroupModel : ViewModel() { 10 | var version = 0 11 | var isInLikeMode = false 12 | 13 | private val _tvGroup = MutableLiveData>() 14 | val tvGroup: LiveData> 15 | get() = _tvGroup 16 | val tvGroupValue: List 17 | get() = _tvGroup.value ?: emptyList() 18 | 19 | private val _position = MutableLiveData() 20 | val position: LiveData 21 | get() = _position 22 | val positionValue: Int 23 | get() = _position.value ?: 0 24 | 25 | fun setPosition(position: Int) { 26 | _position.value = position 27 | } 28 | 29 | private val _positionPlaying = MutableLiveData() 30 | val positionPlaying: LiveData 31 | get() = _positionPlaying 32 | val positionPlayingValue: Int 33 | get() = _positionPlaying.value ?: DEFAULT_POSITION_PLAYING 34 | 35 | fun setPositionPlaying(position: Int) { 36 | _positionPlaying.value = position 37 | SP.positionGroup = position 38 | } 39 | 40 | fun setPositionPlaying() { 41 | setPositionPlaying(positionValue) 42 | } 43 | 44 | private val _change = MutableLiveData() 45 | val change: LiveData 46 | get() = _change 47 | 48 | fun setChange() { 49 | _change.value = version 50 | version++ 51 | } 52 | 53 | fun setTVListModelList(tvGroup: List) { 54 | _tvGroup.value = tvGroup 55 | } 56 | 57 | fun addTVListModel(listTVModel: TVListModel) { 58 | _tvGroup.value = tvGroupValue.toMutableList().apply { 59 | add(listTVModel) 60 | } 61 | } 62 | 63 | fun getTVListModel(): TVListModel? { 64 | return getTVListModel(positionValue) 65 | } 66 | 67 | fun getTVListModel(idx: Int): TVListModel? { 68 | if (idx < 0 || idx >= size()) { 69 | return null 70 | } 71 | 72 | if (SP.showAllChannels) { 73 | return tvGroupValue[idx] 74 | } 75 | 76 | return tvGroupValue.filterIndexed { index, _ -> index != 1 }[idx] 77 | } 78 | 79 | private fun getTVListModelNotFilter(idx: Int): TVListModel? { 80 | if (idx < 0 || idx >= tvGroupValue.size) { 81 | return null 82 | } 83 | 84 | return tvGroupValue[idx] 85 | } 86 | 87 | // get & set 88 | fun getPosition(position: Int): TVModel? { 89 | 90 | // No item 91 | if (tvGroupValue[1].size() == 0) { 92 | return null 93 | } 94 | 95 | var count = 0 96 | for ((index, i) in tvGroupValue.withIndex()) { 97 | val countBefore = count 98 | count += i.size() 99 | if (count > position) { 100 | setPosition(index) 101 | val listPosition = position - countBefore 102 | i.setPosition(listPosition) 103 | return i.getTVModel(listPosition) 104 | } 105 | } 106 | 107 | return null 108 | } 109 | 110 | fun getCurrent(): TVModel? { 111 | 112 | // No item 113 | if (tvGroupValue.size < 3 || tvGroupValue[1].size() == 0) { 114 | return null 115 | } 116 | 117 | return getCurrentList()?.getCurrent() 118 | } 119 | 120 | fun getCurrentList(): TVListModel? { 121 | return getTVListModelNotFilter(positionValue) 122 | } 123 | 124 | fun getFavoritesList(): TVListModel? { 125 | return getTVListModelNotFilter(0) 126 | } 127 | 128 | fun getAllList(): TVListModel? { 129 | return getTVListModelNotFilter(1) 130 | } 131 | 132 | // get & set 133 | // keep: In the current list loop 134 | fun getPrev(keep: Boolean = false): TVModel? { 135 | // No item 136 | if (tvGroupValue.size < 3 || tvGroupValue[1].size() == 0) { 137 | return null 138 | } 139 | 140 | var tvListModel = getCurrentList() ?: return null 141 | 142 | if (keep) { 143 | return tvListModel.getPrev() 144 | } 145 | 146 | // Prev tvListModel 147 | if (tvListModel.positionPlayingValue == 0) { 148 | var p = (tvGroupValue.size + positionPlayingValue - 1) % tvGroupValue.size 149 | if (p == 1 || p == 0) { 150 | // 最後一組 151 | p = (tvGroupValue.size - 1) % tvGroupValue.size 152 | } 153 | setPositionPlaying(p) 154 | setPosition(p) 155 | 156 | // Log.i(TAG, "group positionPlaying $p/${tvGroupValue.size - 1}") 157 | tvListModel = getTVListModelNotFilter(p)!! 158 | return tvListModel.getTVModel(tvListModel.size() - 1) 159 | } 160 | 161 | return tvListModel.getPrev() 162 | } 163 | 164 | // get & set 165 | fun getNext(keep: Boolean = false): TVModel? { 166 | // No item 167 | if (tvGroupValue.size < 3 || tvGroupValue[1].size() == 0) { 168 | return null 169 | } 170 | 171 | var tvListModel = getCurrentList() ?: return null 172 | 173 | if (keep) { 174 | return tvListModel.getNext() 175 | } 176 | 177 | // Next tvListModel 178 | if (tvListModel.positionPlayingValue == tvListModel.size() - 1) { 179 | var p = (positionPlayingValue + 1) % tvGroupValue.size 180 | if (p == 0) { 181 | // 第一組 182 | p = 2 183 | } 184 | setPositionPlaying(p) 185 | setPosition(p) 186 | 187 | // Log.i(TAG, "group positionPlaying $p/${tvGroupValue.size - 1}") 188 | tvListModel = getTVListModelNotFilter(p)!! 189 | return tvListModel.getTVModel(0) 190 | } 191 | 192 | return tvListModel.getNext() 193 | } 194 | 195 | fun defaultPosition(): Int { 196 | // 1 全部 197 | // 2 第一組 198 | return if (tvGroupValue.size > 2) 2 else 1 199 | } 200 | 201 | fun initTVGroup() { 202 | _tvGroup.value = mutableListOf( 203 | tvGroupValue[0], 204 | tvGroupValue[1] 205 | ) 206 | tvGroupValue[1].initTVList() 207 | } 208 | 209 | fun initPosition() { 210 | setPosition(defaultPosition()) 211 | setPositionPlaying() 212 | } 213 | 214 | init { 215 | setPosition(SP.positionGroup) 216 | setPositionPlaying() 217 | isInLikeMode = SP.defaultLike && positionValue == 0 218 | } 219 | 220 | fun size(): Int { 221 | if (SP.showAllChannels) { 222 | return tvGroupValue.size 223 | } 224 | 225 | return tvGroupValue.size - 1 226 | } 227 | 228 | companion object { 229 | const val TAG = "TVGroupModel" 230 | const val DEFAULT_POSITION_PLAYING = -1 231 | } 232 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/models/TVListModel.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0.models 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import com.lizongying.mytv0.SP 8 | 9 | class TVListModel(private val name: String, private val groupIndex: Int) : ViewModel() { 10 | var version = 0 11 | 12 | private val _removed = MutableLiveData>() 13 | val removed: LiveData> 14 | get() = _removed 15 | 16 | private val _added = MutableLiveData>() 17 | val added: LiveData> 18 | get() = _added 19 | 20 | private val _changed = MutableLiveData>() 21 | val changed: LiveData> 22 | get() = _changed 23 | 24 | fun getName(): String { 25 | return name 26 | } 27 | 28 | // position in tvGroup. No filters 29 | fun getGroupIndex(): Int { 30 | return groupIndex 31 | } 32 | 33 | private val _tvList = MutableLiveData>() 34 | val tvList: LiveData> 35 | get() = _tvList 36 | private val tvListValue: List 37 | get() = _tvList.value ?: emptyList() 38 | 39 | private val _position = MutableLiveData() 40 | val position: LiveData 41 | get() = _position 42 | val positionValue: Int 43 | get() = _position.value ?: 0 44 | 45 | fun setPosition(position: Int) { 46 | _position.value = position 47 | } 48 | 49 | private val _positionPlaying = MutableLiveData() 50 | val positionPlaying: LiveData 51 | get() = _positionPlaying 52 | val positionPlayingValue: Int 53 | get() = _positionPlaying.value ?: 0 54 | 55 | fun setPositionPlaying(position: Int) { 56 | _positionPlaying.value = position 57 | SP.position = position 58 | } 59 | 60 | fun setPositionPlaying() { 61 | setPositionPlaying(positionValue) 62 | } 63 | 64 | private val _change = MutableLiveData() 65 | val change: LiveData 66 | get() = _change 67 | 68 | fun setChange() { 69 | _change.value = true 70 | } 71 | 72 | fun setTVListModel(tvList: List) { 73 | _tvList.value = tvList 74 | } 75 | 76 | fun addTVModel(tvModel: TVModel) { 77 | _tvList.value = tvListValue.toMutableList().apply { 78 | add(tvModel) 79 | } 80 | 81 | _added.value = Pair(tvListValue.size - 1, version) 82 | version++ 83 | } 84 | 85 | fun removeTVModel(id: Int) { 86 | if (tvListValue.isEmpty()) { 87 | return 88 | } 89 | 90 | val index = tvListValue.indexOfFirst { it.tv.id == id } 91 | if (index != -1) { 92 | _tvList.value = tvListValue.toMutableList().apply { 93 | removeAt(index) 94 | } 95 | 96 | _removed.value = Pair(index, version) 97 | version++ 98 | } 99 | } 100 | 101 | fun replaceTVModel(tvModel: TVModel) { 102 | if (_tvList.value == null) { 103 | _tvList.value = mutableListOf(tvModel) 104 | } 105 | 106 | val index = tvListValue.indexOfFirst { it.tv.id == tvModel.tv.id } 107 | if (index == -1) { 108 | _tvList.value = tvListValue.toMutableList().apply { 109 | add(tvModel) 110 | } 111 | 112 | _added.value = Pair(tvListValue.size - 1, version) 113 | version++ 114 | } 115 | } 116 | 117 | fun getTVModel(): TVModel? { 118 | return getTVModel(positionValue) 119 | } 120 | 121 | fun getTVModel(idx: Int): TVModel? { 122 | if (idx < 0 || idx >= size()) { 123 | return null 124 | } 125 | 126 | setPosition(idx) 127 | return tvListValue[idx] 128 | } 129 | 130 | fun getCurrent(): TVModel? { 131 | if (positionValue < 0 || positionValue >= size()) { 132 | return getTVModel(0) 133 | } 134 | 135 | return getTVModel(positionValue) 136 | } 137 | 138 | fun getPrev(): TVModel? { 139 | if (size() == 0) { 140 | return null 141 | } 142 | 143 | val p = (size() + positionPlayingValue - 1) % size() 144 | setPositionPlaying(p) 145 | setPosition(p) 146 | return tvListValue[p] 147 | } 148 | 149 | fun getNext(): TVModel? { 150 | if (size() == 0) { 151 | return null 152 | } 153 | 154 | val p = (positionPlayingValue + 1) % size() 155 | setPositionPlaying(p) 156 | setPosition(p) 157 | return tvListValue[p] 158 | } 159 | 160 | fun initTVList() { 161 | _tvList.value = mutableListOf() 162 | } 163 | 164 | init { 165 | _position.value = SP.position 166 | } 167 | 168 | fun size(): Int { 169 | return tvListValue.size 170 | } 171 | 172 | companion object { 173 | const val TAG = "TVListModel" 174 | } 175 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/models/TVModel.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0.models 2 | 3 | import android.net.Uri 4 | import androidx.annotation.OptIn 5 | import androidx.lifecycle.LiveData 6 | import androidx.lifecycle.MutableLiveData 7 | import androidx.lifecycle.ViewModel 8 | import androidx.media3.common.MediaItem 9 | import androidx.media3.common.util.UnstableApi 10 | import androidx.media3.datasource.DataSource 11 | import androidx.media3.datasource.okhttp.OkHttpDataSource 12 | import androidx.media3.datasource.rtmp.RtmpDataSource 13 | import androidx.media3.exoplayer.dash.DashMediaSource 14 | import androidx.media3.exoplayer.hls.HlsMediaSource 15 | import androidx.media3.exoplayer.rtsp.RtspMediaSource 16 | import androidx.media3.exoplayer.source.MediaSource 17 | import androidx.media3.exoplayer.source.ProgressiveMediaSource 18 | import com.lizongying.mytv0.SP 19 | import com.lizongying.mytv0.data.EPG 20 | import com.lizongying.mytv0.data.SourceType 21 | import com.lizongying.mytv0.data.TV 22 | import com.lizongying.mytv0.requests.HttpClient 23 | import kotlin.math.max 24 | import kotlin.math.min 25 | 26 | class TVModel(var tv: TV) : ViewModel() { 27 | var retryTimes = 0 28 | var retryMaxTimes = 10 29 | var programUpdateTime = 0L 30 | 31 | private var _groupIndex = 0 32 | val groupIndex: Int 33 | get() = if (SP.showAllChannels || _groupIndex == 0) _groupIndex else _groupIndex - 1 34 | 35 | fun setGroupIndex(index: Int) { 36 | _groupIndex = index 37 | } 38 | 39 | fun getGroupIndexInAll(): Int { 40 | return _groupIndex 41 | } 42 | 43 | var listIndex = 0 44 | 45 | private var sourceTypeList: List = 46 | listOf( 47 | SourceType.UNKNOWN, 48 | ) 49 | private var sourceTypeIndex = 0 50 | 51 | private val _errInfo = MutableLiveData() 52 | val errInfo: LiveData 53 | get() = _errInfo 54 | 55 | fun setErrInfo(info: String) { 56 | _errInfo.value = info 57 | } 58 | 59 | private var _epg = MutableLiveData>() 60 | val epg: LiveData> 61 | get() = _epg 62 | val epgValue: List 63 | get() = _epg.value ?: emptyList() 64 | 65 | fun setEpg(epg: List) { 66 | _epg.value = epg 67 | } 68 | 69 | private val _videoIndex = MutableLiveData() 70 | val videoIndex: LiveData 71 | get() = _videoIndex 72 | val videoIndexValue: Int 73 | get() = _videoIndex.value ?: 0 74 | 75 | fun getVideoUrl(): String? { 76 | if (videoIndexValue >= tv.uris.size) { 77 | return null 78 | } 79 | 80 | return tv.uris[videoIndexValue] 81 | } 82 | 83 | private val _like = MutableLiveData() 84 | val like: LiveData 85 | get() = _like 86 | 87 | fun setLike(liked: Boolean) { 88 | _like.value = liked 89 | } 90 | 91 | private val _ready = MutableLiveData() 92 | val ready: LiveData 93 | get() = _ready 94 | 95 | fun setReady(retry: Boolean = false) { 96 | if (!retry) { 97 | setErrInfo("") 98 | retryTimes = 0 99 | 100 | _videoIndex.value = max(0, min(tv.uris.size - 1, tv.videoIndex)) 101 | sourceTypeIndex = 102 | max(0, min(sourceTypeList.size - 1, sourceTypeList.indexOf(tv.sourceType))) 103 | } 104 | _ready.value = true 105 | } 106 | 107 | private var userAgent = "" 108 | 109 | private var _httpDataSource: DataSource.Factory? = null 110 | private var _mediaItem: MediaItem? = null 111 | 112 | @OptIn(UnstableApi::class) 113 | fun getMediaItem(): MediaItem? { 114 | _mediaItem = getVideoUrl()?.let { 115 | val uri = Uri.parse(it) ?: return@let null 116 | val path = uri.path ?: return@let null 117 | val scheme = uri.scheme ?: return@let null 118 | 119 | val okHttpDataSource = OkHttpDataSource.Factory(HttpClient.okHttpClient) 120 | tv.headers?.let { i -> 121 | okHttpDataSource.setDefaultRequestProperties(i) 122 | i.forEach { (key, value) -> 123 | if (key.equals("user-agent", ignoreCase = true)) { 124 | userAgent = value 125 | return@forEach 126 | } 127 | } 128 | } 129 | 130 | _httpDataSource = okHttpDataSource 131 | 132 | sourceTypeList = if (path.lowercase().endsWith(".m3u8")) { 133 | listOf(SourceType.HLS) 134 | } else if (path.lowercase().endsWith(".mpd")) { 135 | listOf(SourceType.DASH) 136 | } else if (scheme.lowercase() == "rtsp") { 137 | listOf(SourceType.RTSP) 138 | } else if (scheme.lowercase() == "rtmp") { 139 | listOf(SourceType.RTMP) 140 | } else if (scheme.lowercase() == "rtp") { 141 | listOf(SourceType.RTP) 142 | } else { 143 | listOf(SourceType.HLS, SourceType.PROGRESSIVE) 144 | } 145 | 146 | MediaItem.fromUri(it) 147 | } 148 | return _mediaItem 149 | } 150 | 151 | fun getSourceTypeDefault(): SourceType { 152 | return tv.sourceType 153 | } 154 | 155 | fun getSourceTypeCurrent(): SourceType { 156 | sourceTypeIndex = max(0, min(sourceTypeList.size - 1, sourceTypeIndex)) 157 | return sourceTypeList[sourceTypeIndex] 158 | } 159 | 160 | fun nextSourceType(): Boolean { 161 | sourceTypeIndex = (sourceTypeIndex + 1) % sourceTypeList.size 162 | 163 | return sourceTypeIndex == sourceTypeList.size - 1 164 | } 165 | 166 | fun confirmSourceType() { 167 | // TODO save default sourceType 168 | tv.sourceType = getSourceTypeCurrent() 169 | } 170 | 171 | fun confirmVideoIndex() { 172 | tv.videoIndex = videoIndexValue 173 | } 174 | 175 | @OptIn(UnstableApi::class) 176 | fun getMediaSource(): MediaSource? { 177 | if (sourceTypeList.isEmpty()) { 178 | return null 179 | } 180 | 181 | if (_mediaItem == null) { 182 | return null 183 | } 184 | val mediaItem = _mediaItem!! 185 | 186 | if (_httpDataSource == null) { 187 | return null 188 | } 189 | val httpDataSource = _httpDataSource!! 190 | 191 | return when (getSourceTypeCurrent()) { 192 | SourceType.HLS -> HlsMediaSource.Factory(httpDataSource).createMediaSource(mediaItem) 193 | SourceType.RTSP -> if (userAgent.isEmpty()) { 194 | RtspMediaSource.Factory().createMediaSource(mediaItem) 195 | } else { 196 | RtspMediaSource.Factory().setUserAgent(userAgent).createMediaSource(mediaItem) 197 | } 198 | 199 | SourceType.RTMP -> { 200 | val rtmpDataSource = RtmpDataSource.Factory() 201 | ProgressiveMediaSource.Factory(rtmpDataSource) 202 | .createMediaSource(mediaItem) 203 | } 204 | 205 | SourceType.RTP -> null 206 | 207 | SourceType.DASH -> DashMediaSource.Factory(httpDataSource).createMediaSource(mediaItem) 208 | SourceType.PROGRESSIVE -> ProgressiveMediaSource.Factory(httpDataSource) 209 | .createMediaSource(mediaItem) 210 | 211 | else -> null 212 | } 213 | } 214 | 215 | fun isLastVideo(): Boolean { 216 | return videoIndexValue == tv.uris.size - 1 217 | } 218 | 219 | fun nextVideo(): Boolean { 220 | if (tv.uris.isEmpty()) { 221 | return false 222 | } 223 | 224 | _videoIndex.value = (videoIndexValue + 1) % tv.uris.size 225 | sourceTypeList = listOf( 226 | SourceType.UNKNOWN, 227 | ) 228 | 229 | return isLastVideo() 230 | } 231 | 232 | fun update(t: TV) { 233 | tv = t 234 | } 235 | 236 | init { 237 | _videoIndex.value = max(0, min(tv.uris.size - 1, tv.videoIndex)) 238 | _like.value = SP.getLike(tv.id) 239 | } 240 | 241 | companion object { 242 | private const val TAG = "TVModel" 243 | } 244 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/requests/DnsCache.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0.requests 2 | 3 | import okhttp3.Dns 4 | import java.net.Inet4Address 5 | import java.net.Inet6Address 6 | import java.net.InetAddress 7 | import java.util.concurrent.ConcurrentHashMap 8 | 9 | 10 | class DnsCache : Dns { 11 | private val dnsCache = ConcurrentHashMap>() 12 | 13 | override fun lookup(hostname: String): List { 14 | if (hostname.isEmpty()) { 15 | return Dns.SYSTEM.lookup(hostname); 16 | } 17 | 18 | dnsCache[hostname]?.let { 19 | return it 20 | } 21 | 22 | val ipv4Addresses = mutableListOf() 23 | val ipv6Addresses = mutableListOf() 24 | 25 | for (address in InetAddress.getAllByName(hostname)) { 26 | if (address is Inet4Address) { 27 | ipv4Addresses.add(address) 28 | } else if (address is Inet6Address) { 29 | ipv6Addresses.add(address) 30 | } 31 | } 32 | 33 | val addressesNew = ipv4Addresses + ipv6Addresses 34 | 35 | if (addressesNew.isNotEmpty()) { 36 | dnsCache[hostname] = addressesNew 37 | } 38 | 39 | return addressesNew 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/requests/HttpClient.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0.requests 2 | 3 | 4 | import android.net.Uri 5 | import android.os.Build 6 | import android.util.Log 7 | import com.lizongying.mytv0.SP 8 | import com.lizongying.mytv0.Utils.formatUrl 9 | import okhttp3.ConnectionSpec 10 | import okhttp3.OkHttpClient 11 | import okhttp3.TlsVersion 12 | import java.net.InetSocketAddress 13 | import java.net.Proxy 14 | import java.security.KeyStore 15 | import javax.net.ssl.SSLContext 16 | import javax.net.ssl.TrustManagerFactory 17 | import javax.net.ssl.X509TrustManager 18 | 19 | 20 | object HttpClient { 21 | const val TAG = "HttpClient" 22 | 23 | private val clientCache = mutableMapOf() 24 | 25 | val okHttpClient: OkHttpClient by lazy { 26 | getClientWithProxy() 27 | } 28 | 29 | val builder: OkHttpClient.Builder by lazy { 30 | createBuilder() 31 | } 32 | 33 | fun getClientWithProxy(): OkHttpClient { 34 | clientCache[SP.proxy]?.let { 35 | return it 36 | } 37 | 38 | if (!SP.proxy.isNullOrEmpty()) { 39 | try { 40 | val proxyUri = Uri.parse(formatUrl(SP.proxy!!)) 41 | val proxyType = when (proxyUri.scheme) { 42 | "http", "https" -> Proxy.Type.HTTP 43 | "socks", "socks5" -> Proxy.Type.SOCKS 44 | else -> null 45 | } 46 | proxyType?.let { 47 | builder.proxy(Proxy(it, InetSocketAddress(proxyUri.host, proxyUri.port))) 48 | } 49 | Log.i(TAG, "apply proxy $proxyUri") 50 | } catch (e: Exception) { 51 | Log.e(TAG, "getClientWithProxy", e) 52 | } 53 | } 54 | 55 | val client = builder.build() 56 | clientCache[SP.proxy] = client 57 | return client 58 | } 59 | 60 | private fun createBuilder(): OkHttpClient.Builder { 61 | val trustManager = 62 | object : X509TrustManager { 63 | override fun checkClientTrusted( 64 | chain: Array?, 65 | authType: String? 66 | ) { 67 | } 68 | 69 | override fun checkServerTrusted( 70 | chain: Array?, 71 | authType: String? 72 | ) { 73 | } 74 | 75 | override fun getAcceptedIssuers(): Array { 76 | return emptyArray() 77 | } 78 | } 79 | 80 | val sslContext = SSLContext.getInstance("TLS") 81 | sslContext.init(null, arrayOf(trustManager), java.security.SecureRandom()) 82 | 83 | return OkHttpClient.Builder() 84 | .sslSocketFactory(sslContext.socketFactory, trustManager) 85 | .hostnameVerifier { _, _ -> true } 86 | .connectionSpecs(listOf(ConnectionSpec.COMPATIBLE_TLS, ConnectionSpec.CLEARTEXT)) 87 | .dns(DnsCache()) 88 | .apply { enableTls12OnPreLollipop() } 89 | } 90 | 91 | private fun OkHttpClient.Builder.enableTls12OnPreLollipop() { 92 | if (Build.VERSION.SDK_INT < 22) { 93 | try { 94 | val sslContext = SSLContext.getInstance("TLSv1.2") 95 | sslContext.init(null, null, java.security.SecureRandom()) 96 | 97 | val trustManagerFactory = TrustManagerFactory.getInstance( 98 | TrustManagerFactory.getDefaultAlgorithm() 99 | ) 100 | trustManagerFactory.init(null as KeyStore?) 101 | val trustManagers = trustManagerFactory.trustManagers 102 | val trustManager = trustManagers[0] as X509TrustManager 103 | 104 | sslSocketFactory(Tls12SocketFactory(sslContext.socketFactory), trustManager) 105 | connectionSpecs( 106 | listOf( 107 | ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) 108 | .tlsVersions(TlsVersion.TLS_1_2) 109 | .build(), 110 | ConnectionSpec.COMPATIBLE_TLS, 111 | ConnectionSpec.CLEARTEXT 112 | ) 113 | ) 114 | } catch (e: Exception) { 115 | Log.e(TAG, "enableTls12OnPreLollipop", e) 116 | } 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lizongying/mytv0/requests/Tls12SocketFactory.kt: -------------------------------------------------------------------------------- 1 | package com.lizongying.mytv0.requests 2 | 3 | import java.io.IOException 4 | import java.net.InetAddress 5 | import java.net.Socket 6 | import java.net.UnknownHostException 7 | import javax.net.ssl.SSLSocket 8 | import javax.net.ssl.SSLSocketFactory 9 | 10 | 11 | /** 12 | * Enables TLS v1.2 when creating SSLSockets. 13 | * 14 | * 15 | * For some reason, android supports TLS v1.2 from API 16, but enables it by 16 | * default only from API 20. 17 | * @link https://developer.android.com/reference/javax/net/ssl/SSLSocket.html 18 | * @see SSLSocketFactory 19 | */ 20 | class Tls12SocketFactory(private val delegate: SSLSocketFactory) : SSLSocketFactory() { 21 | override fun getDefaultCipherSuites(): Array { 22 | return delegate.defaultCipherSuites 23 | } 24 | 25 | override fun getSupportedCipherSuites(): Array { 26 | return delegate.supportedCipherSuites 27 | } 28 | 29 | @Throws(IOException::class) 30 | override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket { 31 | return patch(delegate.createSocket(s, host, port, autoClose)) 32 | } 33 | 34 | @Throws(IOException::class, UnknownHostException::class) 35 | override fun createSocket(host: String, port: Int): Socket { 36 | return patch(delegate.createSocket(host, port)) 37 | } 38 | 39 | @Throws(IOException::class, UnknownHostException::class) 40 | override fun createSocket( 41 | host: String, 42 | port: Int, 43 | localHost: InetAddress, 44 | localPort: Int 45 | ): Socket { 46 | return patch(delegate.createSocket(host, port, localHost, localPort)) 47 | } 48 | 49 | @Throws(IOException::class) 50 | override fun createSocket(host: InetAddress, port: Int): Socket { 51 | return patch(delegate.createSocket(host, port)) 52 | } 53 | 54 | @Throws(IOException::class) 55 | override fun createSocket( 56 | address: InetAddress, 57 | port: Int, 58 | localAddress: InetAddress, 59 | localPort: Int 60 | ): Socket { 61 | return patch(delegate.createSocket(address, port, localAddress, localPort)) 62 | } 63 | 64 | private fun patch(s: Socket): Socket { 65 | if (s is SSLSocket) { 66 | s.enabledProtocols = TLS_V12_ONLY 67 | } 68 | return s 69 | } 70 | 71 | companion object { 72 | private val TLS_V12_ONLY = arrayOf("TLSv1.2") 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/res/color/switch_thumb_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/color/switch_track_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/appreciate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-0/19ab99c4c2f819fdfd08b09f64219bb7cd93eaa1/app/src/main/res/drawable/appreciate.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/banner0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-0/19ab99c4c2f819fdfd08b09f64219bb7cd93eaa1/app/src/main/res/drawable/banner0.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_done_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_favorite_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_favorite_border_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_sentiment_dissatisfied_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/custom_progress_drawable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/light_mode_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/logo0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-0/19ab99c4c2f819fdfd08b09f64219bb7cd93eaa1/app/src/main/res/drawable/logo0.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_dark_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_dark_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_dark_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_light_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_white_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_white_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_white_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/volume_off_24px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/volume_up_24px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/channel.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 13 | 14 | 22 | 32 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/layout/error.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 22 | 23 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/group_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/info.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 13 | 20 | 21 | 29 | 38 | 39 | 47 | 48 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 20 | 21 | 35 | 36 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 18 | 19 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/modal.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/player.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 24 | 25 | 34 | 35 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/layout/program.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/program_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 25 | 26 | 40 | -------------------------------------------------------------------------------- /app/src/main/res/layout/settings_web.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/show.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/sources.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 25 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/layout/sources_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 24 | 25 | 38 | 39 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/layout/time.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 13 | 14 | 22 | 32 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-0/19ab99c4c2f819fdfd08b09f64219bb7cd93eaa1/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-0/19ab99c4c2f819fdfd08b09f64219bb7cd93eaa1/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-0/19ab99c4c2f819fdfd08b09f64219bb7cd93eaa1/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-0/19ab99c4c2f819fdfd08b09f64219bb7cd93eaa1/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-0/19ab99c4c2f819fdfd08b09f64219bb7cd93eaa1/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/raw/sources.txt: -------------------------------------------------------------------------------- 1 | ䷯䷋䷃䷵䷟䷋䷽䷄䷴䷻䷍䷻䷭䷅䷟䷰䷑䷹䷔䷵䷯䷋䷿䷂䷱䷅䷷䷔䷟䷐䷷䷌䷸䷘䷃䷔䷸䷘䷆䷰䷭䷹䷍䷝䷴䷹䷨䷕䷛䷐䷍䷰䷑䷉䷔䷕䷸䷐䷟䷌䷸䷅䷲䷝䷱䷋䷭䷝䷽䷇䷍䷝䷭䷮䷔䷰䷴䷹䷢䷒䷟䷇䷍䷼䷟䷐䷽䷌䷸䷮䷢䷕䷸䷂䷍䷻䷑䷅䷽䷌䷟䷐䷢䷉䷴䷹䷥䷌䷭䷐䷔䷶䷑䷜䷙䷵䷛䷋䷆䷦䷯䷋䷃䷵䷟䷋䷽䷄䷴䷻䷍䷻䷭䷅䷟䷰䷑䷹䷔䷵䷯䷋䷿䷂䷱䷅䷷䷔䷟䷐䷷䷌䷸䷘䷃䷔䷸䷘䷆䷰䷭䷹䷍䷝䷴䷹䷨䷕䷛䷐䷍䷰䷑䷉䷔䷕䷸䷐䷟䷌䷸䷅䷲䷝䷱䷋䷭䷝䷽䷇䷍䷝䷭䷮䷔䷰䷴䷹䷢䷒䷟䷇䷍䷼䷟䷐䷽䷌䷸䷮䷢䷕䷸䷂䷍䷻䷑䷅䷽䷌䷟䷐䷢䷉䷴䷹䷷䷣䷭䷮䷙䷰䷑䷮䷨䷼䷴䷘䷃䷊䷱䷁䷕䷣䷱䷋䷃䷒䷟䷼䷣䷌䷴䷉䷳䷚䷱䷻䷙䷘䷯䷅䷃䷣䷱䷮䷳䷥䷟䷹䷿䷻䷭䷹䷍䷰䷱䷬䷿䷰䷱䷇䷙䷩䷸䷹䷵䷌䷑䷐䷢䷰䷸䷮䷔䷰䷑䷹䷥䷕䷸䷐䷟䷌䷸䷬䷔䷹䷑䷜䷍䷝䷭䷮䷔䷰䷴䷉䷃䷹䷴䷹䷵䷼䷱䷜䷍䷕䷟䷋䷭䷹䷴䷐䷵䷼䷱䷆䷕䷣䷱䷋䷃䷒䷟䷼䷣䷌䷴䷉䷳䷚䷱䷻䷙䷘䷯䷅䷃䷣䷱䷮䷳䷥䷟䷹䷿䷻䷭䷹䷍䷰䷱䷬䷿䷰䷱䷇䷙䷩䷸䷹䷵䷌䷑䷐䷢䷰䷸䷮䷔䷰䷑䷹䷥䷕䷸䷐䷟䷌䷸䷬䷔䷹䷑䷜䷍䷝䷭䷮䷔䷰䷴䷉䷃䷹䷴䷹䷵䷼䷱䷜䷍䷕䷱䷋䷭䷰䷸䷺䷷䷥䷇䷐䷚䷵䷱䷋䷖䷼䷞䷂䷡䷌䷟䷐䷢䷉䷴䷐䷱䷕䷱䷬䷚䷥䷭䷘䷿䷼䷑䷅䷳䷩䷸䷹䷙䷵䷑䷮䷙䷵䷴䷐䷷䷌䷸䷜䷍䷐䷭䷮䷙䷝䷯䷮䷙䷘䷸䷮䷔䷰䷑䷻䷍䷶䷯䷅䷑䷔䷴䷹䷥䷚䷯䷮䷊䷌䷱䷋䷭䷌䷸䷺䷷䷥䷴䷹䷔䷰䷑䷬䷿䷊䷴䷐䷵䷼䷱䷆䷕䷣䷱䷋䷃䷒䷟䷼䷣䷌䷴䷉䷳䷚䷱䷻䷙䷘䷯䷅䷃䷣䷱䷮䷳䷥䷟䷹䷿䷻䷭䷹䷍䷰䷱䷬䷿䷰䷱䷇䷙䷩䷸䷹䷵䷌䷃䷉䷿䷌䷱䷐䷔䷰䷴䷹䷔䷒䷱䷋䷭䷝䷭䷅䷖䷕䷴䷹䷱䷲䷴䷹䷍䷥䷱䷋䷖䷥䷱䷇䷍䷻䷑䷅䷷䷥䷸䷋䷆䷰䷸䷺䷷䷥䷇䷐䷚䷵䷱䷋䷖䷼䷞䷂䷡䷌䷟䷐䷢䷉䷴䷐䷱䷕䷱䷬䷚䷥䷭䷘䷿䷼䷑䷅䷳䷩䷸䷹䷙䷵䷑䷮䷙䷵䷴䷐䷷䷌䷸䷜䷍䷾䷸䷹䷿䷹䷑䷅䷷䷼䷴䷵䷔䷆䷿䷢䷭䷌䷸䷮䷢䷕䷸䷂䷍䷼䷸䷉䷿䷻䷭䷹䷿䷼䷴䷹䷔䷒䷱䷋䷑䷫䷟䷹䷍䷥䷟䷐䷷䷔䷟䷻䷙䷝䷽䷉䷧䷦䷯䷋䷃䷵䷟䷋䷽䷄䷴䷻䷍䷻䷭䷅䷟䷰䷑䷹䷔䷵䷯䷋䷿䷂䷱䷅䷷䷔䷟䷐䷷䷌䷸䷘䷃䷔䷸䷘䷆䷰䷭䷹䷍䷝䷴䷹䷕䷌䷑䷅䷑䷔䷟䷉䷽䷌䷜䷿䷖䷧䷿䷂䷍䷝䷭䷮䷔䷰䷴䷉䷷䷌䷱䷅䷳䷩䷑䷅䷽䷌䷯䷬䷍䷝䷑䷿䷍䷼䷸䷉䷿䷻䷭䷹䷿䷼䷴䷐䷵䷼䷱䷆䷕䷣䷱䷋䷃䷒䷟䷼䷣䷌䷴䷉䷳䷚䷱䷻䷙䷘䷯䷅䷃䷣䷱䷮䷳䷥䷟䷹䷿䷻䷭䷹䷍䷰䷱䷬䷿䷰䷱䷇䷙䷩䷸䷹䷵䷌䷯䷐䷍䷔䷱䷐䷿䷼䷟䷻䷍䷳䷧䷢䷃䷮䷴䷹䷥䷚䷯䷮䷊䷌䷯䷅䷖䷵䷱䷂䷙䷝䷽䷉䷧䷊䷇䷐䷚䷵䷱䷋䷖䷼䷞䷂䷡䷌䷟䷐䷢䷉䷴䷐䷱䷕䷱䷬䷚䷥䷭䷘䷿䷼䷑䷅䷳䷩䷸䷹䷙䷵䷑䷮䷙䷵䷴䷐䷷䷌䷸䷜䷍䷾䷸䷹䷿䷹䷑䷅䷷䷼䷴䷵䷔䷆䷿䷢䷭䷌䷸䷮䷢䷕䷸䷂䷍䷣䷸䷹䷥䷔䷴䷐䷵䷼䷱䷺䷗䷦䷯䷋䷃䷵䷟䷋䷽䷄䷴䷻䷍䷻䷭䷅䷟䷰䷑䷹䷔䷵䷯䷋䷿䷂䷱䷅䷷䷔䷟䷐䷷䷌䷸䷘䷃䷔䷸䷘䷆䷰䷭䷹䷍䷝䷴䷉䷕䷂䷑䷮䷑䷕䷸䷐䷧䷌䷯䷅䷖䷵䷱䷂䷍䷝䷭䷮䷔䷰䷴䷹䷔䷒䷱䷋䷭䷰䷸䷺䷷䷥䷇䷐䷚䷵䷱䷋䷖䷼䷞䷂䷡䷌䷟䷐䷢䷉䷴䷐䷱䷕䷱䷬䷚䷥䷭䷘䷿䷼䷑䷅䷳䷩䷸䷹䷙䷵䷑䷮䷙䷵䷴䷐䷷䷌䷸䷜䷍䷑䷭䷮䷙䷋䷴䷺䷏䷙䷞䷓䷲䷌䷸䷺䷷䷥䷴䷹䷥䷚䷯䷮䷊䷌䷃䷹䷢䷵䷯䷬䷿䷻䷴䷐䷵䷼䷱䷆䷕䷣䷱䷋䷃䷒䷟䷼䷣䷌䷴䷉䷳䷚䷱䷻䷙䷘䷯䷅䷃䷣䷱䷮䷳䷥䷟䷹䷿䷻䷭䷹䷍䷰䷱䷬䷿䷰䷱䷇䷙䷩䷸䷹䷵䷌䷮䷅䷿䷔䷆䷹䷚䷚䷸䷂䷍䷽䷯䷅䷑䷔䷴䷹䷥䷚䷯䷮䷊䷌䷆䷿䷖䷧䷿䷂䷙䷝䷽䷉䷧䷦䷯䷋䷃䷵䷟䷋䷽䷄䷴䷻䷍䷻䷭䷅䷟䷰䷑䷹䷔䷵䷯䷋䷿䷂䷱䷅䷷䷔䷟䷐䷷䷌䷸䷘䷃䷔䷸䷘䷆䷰䷭䷹䷍䷝䷴䷥䷔䷥䷑䷧䷷䷣䷭䷮䷊䷌䷺䷬䷔䷹䷑䷜䷍䷝䷭䷮䷔䷰䷴䷵䷱䷶䷸䷹䷳䷚䷸䷇䷙䷝䷽䷉䷧䷦䷯䷋䷃䷵䷟䷋䷽䷄䷴䷻䷍䷻䷭䷅䷟䷰䷑䷹䷔䷵䷯䷋䷿䷂䷱䷅䷷䷔䷟䷐䷷䷌䷸䷘䷃䷔䷸䷘䷆䷰䷭䷹䷍䷝䷴䷥䷔䷥䷑䷧䷷䷣䷭䷮䷊䷌䷺䷬䷔䷹䷑䷜䷍䷝䷭䷮䷔䷰䷴䷵䷔䷆䷿䷢䷭䷰䷸䷺䷷䷥䷇䷐䷚䷵䷱䷋䷖䷼䷞䷂䷡䷌䷟䷐䷢䷉䷴䷐䷱䷕䷱䷬䷚䷥䷭䷘䷿䷼䷑䷅䷳䷩䷸䷹䷙䷵䷑䷮䷙䷵䷴䷐䷷䷌䷸䷜䷍䷺䷧䷢䷗䷼䷷䷼䷎䷙䷽䷩䷗䷌䷺䷅䷔䷳䷧䷢䷃䷮䷴䷹䷥䷚䷟䷉䷃䷔䷟䷂䷀䷕䷰䷙䷋䷕䷌䷕䷘䷐䷶䷑䷍䷆䷺䷢䷃䷮䷙䷄䷛䷈䷙䷭䷾䷣䷆䷵䷃䷞䷙䷊䷐䷎䷴䷘䷃䷊䷱䷁䷕䷣䷱䷋䷃䷒䷟䷼䷣䷌䷴䷉䷳䷚䷱䷻䷙䷘䷯䷅䷃䷣䷱䷮䷳䷥䷟䷹䷿䷻䷭䷹䷍䷰䷱䷬䷿䷰䷱䷇䷙䷩䷸䷹䷵䷌䷧䷥䷖䷭䷽䷼䷟䷻䷞䷺䷎䷊䷴䷵䷥䷙䷜䷿䷖䷧䷿䷂䷍䷝䷭䷅䷷䷵䷑䷅䷎䷌䷙䷸䷬䷨䷙䷴䷂䷟䷧䷵䷙䷷䷙䷄䷛䷈䷙䷭䷾䷣䷆䷵䷃䷞䷙䷊䷐䷎䷴䷘䷃䷊䷱䷁䷕䷣䷱䷋䷃䷒䷟䷼䷣䷌䷴䷹䷨䷕䷱䷐䷧䷰䷛䷐䷳䷲䷟䷻䷙䷵䷸䷉䷁䷌䷱䷋䷭䷌䷯䷅䷖䷵䷱䷩䷭䷰䷸䷺䷷䷥䷇䷐䷚䷵䷱䷋䷖䷼䷞䷂䷡䷌䷟䷐䷢䷉䷴䷐䷱䷕䷱䷬䷚䷥䷭䷘䷿䷼䷑䷅䷳䷩䷸䷹䷙䷵䷑䷮䷙䷵䷴䷐䷷䷌䷸䷜䷍䷹䷭䷘䷷䷤䷛䷮䷷䷰䷴䷹䷔䷒䷱䷋䷭䷌䷸䷮䷢䷼䷱䷬䷿䷻䷴䷉䷃䷹䷴䷹䷔䷒䷱䷋䷭䷵䷴䷐䷵䷼䷱䷆䷕䷣䷱䷋䷃䷒䷞䷂䷡䷌䷽䷺䷟䷥䷴䷩䷏䷉䷞䷇䷊䷻䷷䷺䷏䷰䷽䷺䷗䷼䷞䷩䷭䷹䷞䷓䷲䷌䷸䷬䷔䷹䷑䷜䷙䷝䷽䷉䷧䷦䷯䷋䷃䷵䷟䷋䷽䷄䷴䷻䷍䷻䷭䷅䷟䷰䷑䷹䷔䷵䷯䷋䷿䷂䷱䷅䷷䷔䷟䷐䷷䷌䷸䷘䷃䷔䷸䷘䷆䷰䷭䷹䷍䷝䷴䷵䷳䷥䷟䷐䷙䷕䷸䷐䷱䷓䷷䷇䷍䷓䷯䷬䷔䷰䷑䷅䷷䷔䷴䷧䷔䷆䷿䷢䷭䷌䷸䷮䷢䷼䷱䷬䷿䷻䷴䷥䷃䷮䷴䷧䷔䷆䷿䷩䷆䷰䷸䷺䷷䷥䷇䷐䷚䷵䷱䷋䷖䷼䷞䷂䷡䷌䷟䷐䷢䷉䷴䷐䷱䷕䷱䷬䷚䷥䷭䷘䷿䷼䷑䷅䷳䷩䷸䷹䷙䷵䷑䷮䷙䷵䷴䷐䷷䷌䷸䷜䷡䷙䷷䷺䷎䷉䷛䷬䷔䷚䷸䷼䷲䷥䷽䷩䷟䷌䷯䷅䷖䷵䷱䷂䷍䷝䷭䷮䷔䷰䷴䷹䷔䷒䷱䷋䷭䷰䷱䷋䷚䷵䷇䷐䷚䷵䷱䷋䷖䷼䷞䷂䷡䷌䷟䷐䷢䷉䷴䷐䷱䷕䷱䷬䷚䷥䷭䷘䷿䷼䷑䷅䷳䷩䷸䷹䷙䷵䷑䷮䷙䷵䷴䷐䷷䷌䷸䷜䷍䷣䷱䷮䷢䷰䷑䷼䷟䷉䷽䷓䷏䷒䷽䷜䷍䷝䷛䷜䷥䷕䷟䷋䷃䷹䷴䷹䷥䷚䷯䷮䷊䷌䷜䷿䷖䷧䷿䷂䷥䷕䷟䷋䷭䷵䷴䷐䷵䷼䷱䷆䷕䷣䷱䷋䷃䷒䷟䷼䷣䷌䷴䷉䷳䷚䷱䷻䷙䷘䷯䷅䷃䷣䷱䷮䷳䷥䷟䷹䷿䷻䷭䷹䷍䷰䷱䷬䷿䷰䷱䷇䷙䷩䷸䷹䷵䷌䷯䷋䷿䷚䷸䷐䷟䷉䷷䷼䷁䷨䷽䷓䷏䷌䷸䷅䷲䷝䷯䷅䷖䷵䷱䷂䷍䷝䷭䷮䷔䷰䷴䷵䷔䷆䷿䷢䷭䷝䷯䷅䷖䷹䷷䷂䷙䷝䷽䷉䷧䷦䷯䷋䷃䷵䷟䷋䷽䷄䷴䷻䷍䷻䷭䷅䷟䷰䷑䷹䷔䷵䷯䷋䷿䷂䷱䷅䷷䷔䷟䷐䷷䷌䷸䷘䷃䷔䷸䷘䷆䷰䷭䷹䷍䷝䷴䷹䷥䷚䷯䷅䷃䷔䷸䷓䷎䷒䷽䷩䷁䷌䷯䷅䷖䷵䷱䷂䷥䷼䷑䷮䷨䷐䷴䷅䷿䷼䷑䷜䷍䷝䷭䷮䷔䷰䷴䷹䷔䷒䷱䷋䷭䷰䷸䷺䷷䷥䷇䷐䷚䷵䷱䷋䷖䷼䷞䷂䷡䷌䷟䷐䷢䷉䷴䷐䷱䷕䷱䷬䷚䷥䷭䷘䷿䷼䷑䷅䷳䷩䷸䷹䷙䷵䷑䷮䷙䷵䷴䷐䷷䷌䷸䷜䷍䷾䷯䷅䷷䷌䷛䷅䷖䷥䷭䷂䷍䷕䷟䷋䷃䷹䷴䷹䷥䷚䷯䷮䷊䷌䷯䷅䷖䷹䷷䷇䷙䷝䷽䷉䷧䷦䷯䷋䷃䷵䷟䷋䷽䷄䷴䷻䷍䷻䷭䷅䷟䷰䷑䷹䷔䷵䷯䷋䷿䷂䷱䷅䷷䷔䷟䷐䷷䷌䷸䷘䷃䷔䷸䷘䷆䷰䷭䷹䷍䷝䷴䷹䷕䷕䷟䷹䷍䷙䷟䷋䷿䷂䷴䷹䷔䷒䷱䷋䷭䷌䷸䷮䷢䷕䷸䷂䷍䷕䷟䷋䷭䷵䷅䷼䷎䷰䷸䷺䷷䷥䷇䷐䷚䷵䷱䷋䷖䷼䷞䷂䷡䷌䷟䷐䷢䷉䷴䷐䷱䷕䷱䷬䷚䷥䷭䷘䷿䷼䷑䷅䷳䷩䷸䷹䷙䷵䷑䷮䷙䷵䷴䷐䷷䷌䷸䷜䷍䷾䷯䷅䷷䷌䷛䷅䷖䷥䷭䷂䷍䷕䷟䷋䷃䷹䷴䷹䷥䷚䷯䷮䷊䷌䷯䷅䷖䷹䷷䷂䷙䷝䷽䷉䷧䷦䷯䷋䷃䷵䷟䷋䷽䷄䷴䷻䷍䷻䷭䷅䷟䷰䷑䷹䷔䷵䷯䷋䷿䷂䷱䷅䷷䷔䷟䷐䷷䷌䷸䷘䷃䷔䷸䷘䷆䷰䷭䷹䷍䷝䷴䷹䷕䷕䷟䷹䷍䷙䷟䷋䷿䷂䷴䷹䷔䷒䷱䷋䷭䷌䷸䷮䷢䷕䷸䷂䷍䷕䷟䷋䷭䷹䷅䷼䷎䷰䷸䷺䷷䷥ -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 我的电视·〇 3 | 换台反转 4 | 换台时显示频道号 5 | 更新应用 6 | 开机自启 7 | 赞赏作者 8 | 换视频源 9 | 默认频道 10 | 循环播放时显示视频信息 11 | 显示时间 12 | 应用启动后更新视频源 13 | 退出应用 14 | 默认频道号 15 | 恢复默认 16 | 远程配置 17 | 应用启动后进入我的收藏 18 | 网络代理 19 | EPG状态错误 20 | EPG请求错误 21 | 频道导入成功 22 | 频道导入错误 23 | 频道状态错误 24 | 频道格式错误 25 | 无法读取频道 26 | 频道请求错误 27 | 播放错误 28 | 文件不存在 29 | 频道为空 30 | 已恢复到默认配置 31 | 无效的配置地址 32 | 授权失败 33 | 播放默认频道 34 | 播放上次频道 35 | 默认频道超出频道列表范围,已自动设置为0 36 | 上次频道超出频道列表范围,已自动设置为0 37 | 收藏模式 38 | 标准模式 39 | 再按一次退出 40 | 显示全部频道 41 | 默认频道设置成功 42 | 默认频道设置失败 43 | 代理地址设置成功 44 | 代理地址设置失败 45 | EPG设置成功 46 | EPG设置失败 47 | 开始导入频道 48 | 开始设置默认频道 49 | 我的收藏 50 | 全部频道 51 | 紧凑的菜单 52 | 时间显示秒 53 | 暂无视频源 54 | 软解 55 | 节目单为空 56 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 我的電視·〇 3 | 換台反轉 4 | 換台時顯示頻道號 5 | 更新應用 6 | 開機自啟 7 | 讚賞作者 8 | 換視頻源 9 | 默認頻道 10 | 循環播放時顯示視頻信息 11 | 顯示時間 12 | 應用啟動後更新視頻源 13 | 退出應用 14 | 默認頻道號 15 | 恢復默認 16 | 遠程配置 17 | 應用啟動後進入我的收藏 18 | 網絡代理 19 | EPG狀態錯誤 20 | EPG請求錯誤 21 | 頻道導入成功 22 | 頻道導入錯誤 23 | 頻道狀態錯誤 24 | 頻道格式錯誤 25 | 無法讀取頻道 26 | 頻道請求錯誤 27 | 播放錯誤 28 | 文件不存在 29 | 頻道為空 30 | 已恢復到默認配置 31 | 無效的配置地址 32 | 授權失敗 33 | 播放默认頻道 34 | 播放上次頻道 35 | 默认頻道超出頻道列表範圍,已自動設置為0 36 | 上次頻道超出頻道列表範圍,已自動設置為0 37 | 收藏模式 38 | 標準模式 39 | 再按一次退出 40 | 顯示全部頻道 41 | 代理地址設定成功 42 | 代理地址設定失敗 43 | EPG設定成功 44 | EPG設定失敗 45 | 開始導入頻道 46 | 開始設置默認頻道 47 | 我的收藏 48 | 全部頻道 49 | 緊湊的菜單 50 | 時間顯示秒 51 | 暫無視頻源 52 | 軟解 53 | 節目單為空 54 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #0096A6 3 | #FF263238 4 | #000 5 | #FFF 6 | #F00 7 | #FFEEEEEE 8 | #B3EEEEEE 9 | #400096A6 10 | #B3EEEEEE 11 | #0096A6 12 | #FFEEEEEE 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 我的電視·〇 3 | Channel Reversal 4 | Show Channel Number When Switching 5 | Check for Updates 6 | Boot Startup 7 | Appreciate Author 8 | Change Source 9 | Default Channel 10 | Show Video Info During Loop Playback 11 | Show Time 12 | Update Video Source on App Startup 13 | Exit Application 14 | Default Channel Number 15 | Restore Default 16 | Remote Configuration 17 | Enter My Favorites on App Startup 18 | Network Proxy 19 | EPG status error 20 | EPG request error 21 | Channel import successful 22 | Channel import error 23 | Channel status error 24 | Channel format error 25 | Unable to read channel 26 | Channel request error 27 | Play error 28 | File does not exist 29 | Channel does not exist 30 | Configuration restored to default 31 | Invalid configuration address 32 | Authorization failed 33 | Play default channel 34 | Play last channel 35 | Default channel out of range, automatically set to 0 36 | Last channel out of range, automatically set to 0 37 | Favorite Mode 38 | Standard Mode 39 | Press again to exit 40 | Show all channels 41 | Default channel set successfully 42 | Failed to set default channel 43 | Proxy set successfully 44 | Failed to set proxy 45 | EPG set successfully 46 | Failed to set EPG 47 | Start import channel 48 | Start setting default channel 49 | My Favorites 50 | All Channels 51 | Compact Menu 52 | Display Seconds 53 | No Video Source 54 | Soft Decode 55 | EPG is empty 56 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/network.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | alias(libs.plugins.android.application) apply false 4 | alias(libs.plugins.kotlin.android) apply false 5 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | desugar_jdk_libs = "2.1.4" 3 | media3 = "1.5.1" # 1.3.0: 19 java8 1.2.1: 17 media3-datasource-okhttp:1.2.1:21 4 | nanohttpd = "2.3.1" 5 | gua64 = "1.4.5" 6 | recyclerview = "1.4.0" 7 | zxing = "3.5.3" 8 | glide = "4.16.0" # java7 9 | 10 | gson = "2.11.0" # 19:2.10.1 11 | okhttp = "4.12.0" 12 | 13 | core_ktx = "1.15.0" 14 | lifecycle = "2.8.7" 15 | constraintlayout = "2.2.1" 16 | coroutines = "1.8.1" 17 | 18 | android-gradle-plugin = "8.7.3" 19 | kotlin-android = "2.0.0" 20 | appcompat = "1.7.0" 21 | 22 | [libraries] 23 | desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } 24 | 25 | media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3" } 26 | media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } 27 | media3-exoplayer-hls = { module = "androidx.media3:media3-exoplayer-hls", version.ref = "media3" } 28 | media3-exoplayer-dash = { module = "androidx.media3:media3-exoplayer-dash", version.ref = "media3" } 29 | media3-exoplayer-rtsp = { module = "androidx.media3:media3-exoplayer-rtsp", version.ref = "media3" } 30 | media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" } 31 | media3-datasource-rtmp = { module = "androidx.media3:media3-datasource-rtmp", version.ref = "media3" } 32 | 33 | nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" } 34 | gua64 = { module = "io.github.lizongying:gua64", version.ref = "gua64" } 35 | zxing = { module = "com.google.zxing:core", version.ref = "zxing" } 36 | glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } 37 | 38 | gson = { module = "com.google.code.gson:gson", version.ref = "gson" } 39 | okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } 40 | 41 | coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } 42 | constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } 43 | appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } 44 | recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } 45 | core-ktx = { module = "androidx.core:core-ktx", version.ref = "core_ktx" } 46 | lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } 47 | 48 | [plugins] 49 | android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } 50 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin-android" } 51 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-0/19ab99c4c2f819fdfd08b09f64219bb7cd93eaa1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Dec 14 14:50:34 HKT 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /history.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | in_changelog=false 4 | 5 | while IFS= read -r line; do 6 | if [[ "$line" == "## "* ]]; then 7 | continue 8 | fi 9 | 10 | if [[ $in_changelog == false ]] && [[ "$line" == "### "* ]]; then 11 | in_changelog=true 12 | continue 13 | fi 14 | 15 | if [[ $in_changelog == true ]] && [[ "$line" == "### "* ]]; then 16 | break 17 | fi 18 | 19 | echo "$line" 20 | done < HISTORY.md 21 | -------------------------------------------------------------------------------- /screenshots/Screenshot_20240810_151748.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-0/19ab99c4c2f819fdfd08b09f64219bb7cd93eaa1/screenshots/Screenshot_20240810_151748.png -------------------------------------------------------------------------------- /screenshots/Screenshot_20240813_232847.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-0/19ab99c4c2f819fdfd08b09f64219bb7cd93eaa1/screenshots/Screenshot_20240813_232847.png -------------------------------------------------------------------------------- /screenshots/Screenshot_20240813_232900.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-0/19ab99c4c2f819fdfd08b09f64219bb7cd93eaa1/screenshots/Screenshot_20240813_232900.png -------------------------------------------------------------------------------- /screenshots/appreciate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-0/19ab99c4c2f819fdfd08b09f64219bb7cd93eaa1/screenshots/appreciate.png -------------------------------------------------------------------------------- /screenshots/zfb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizongying/my-tv-0/19ab99c4c2f819fdfd08b09f64219bb7cd93eaa1/screenshots/zfb.jpg -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | 16 | rootProject.name = "My TV 0" 17 | include(":app") 18 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | {"version_code": 16976136, "version_name": "v1.3.9.8", "apk_name": "my-tv-0_1.3.9.8.apk", "apk_url": "https://www.gitlink.org.cn/lizongying/my-tv-0/releases/download/v1.3.9.8/my-tv-0_1.3.9.8.apk"} 2 | --------------------------------------------------------------------------------