├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── gradle.xml ├── migrations.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── 046279_arm64v8a_x5.tbs.apk │ ├── 046914_armeabi_x5.tbs.apk │ └── js │ │ ├── backwardScript.js │ │ ├── cctvOpen.js │ │ ├── forwardScript.js │ │ └── getEpgScript.js │ ├── java │ └── com │ │ └── eanyatonic │ │ └── cctvViewer │ │ ├── BrowseErrorActivity.java │ │ ├── CardPresenter.java │ │ ├── ChannelAdapter.java │ │ ├── DetailsActivity.java │ │ ├── DetailsDescriptionPresenter.java │ │ ├── ErrorFragment.java │ │ ├── MainActivity.java │ │ ├── MainFragment.java │ │ ├── Movie.java │ │ ├── MovieList.java │ │ ├── PlaybackActivity.java │ │ ├── PlaybackVideoFragment.java │ │ ├── VideoDetailsFragment.java │ │ ├── bean │ │ └── EpgInfo.java │ │ └── tools │ │ ├── FileTool.java │ │ ├── FileUtils.java │ │ ├── RestartAppUtil.java │ │ └── SysTool.java │ └── res │ ├── drawable │ ├── app_icon_your_company.png │ ├── channel_background_selected.xml │ ├── channel_background_unselected.xml │ ├── default_background.xml │ ├── logo.png │ ├── movie.png │ └── rounded_background.xml │ ├── layout │ ├── activity_details.xml │ ├── activity_main.xml │ └── item_channel.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 │ └── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | .idea/deploymentTargetDropDown.xml 17 | *.apk 18 | app/release/output-metadata.json 19 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | 胖弟看电视 -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

电视浏览器

一个电视机顶盒及Android TV收看中国中央电视台直播的浏览器

2 | 3 | 4 | ## 下载安装包 5 | https://github.com/Zcodeoooo/CCTV_Viewer/releases/ 6 | ![img.png](img.png) 7 | 8 | ## 方案选择 9 | 感谢https://github.com/lizongying/my-tv项目的对于央视频vip接口的支持,作者本人的账号开通的vip造福大众,另外由于该项目本身缺少关键解密文件(其实已经有了扒代码调用解密算法的方式取得ckey参数),还有由于项目本身来自于逆向js算法导致的不稳定性(鉴于最近Pandora项目的归档的教训),本人并不借鉴于此 10 | 另外查看了sekiro方案(多设备注入js,提供负载均衡的服务器接口获取数据),可以达到各个方面的均衡,例如兼容性安卓版本 稳定性 速度,可以达到商业性的标准,但是涉及服务器相关,本人也爱莫能助,有心人可以参考下 11 | 12 | ## 注意 13 | 由于目前精力有限,无法支持对于指定播放地址的模块化协作脚本定制化开发,有其他问题请及时反馈 14 | 15 | ## 兼容性说明 16 | 受WebView 第三方页面限制目前只能在安卓4.4及以上系统正常上运行 17 | 已静态集成arm64和armeabi架构 x5内核 18 | (安卓7.0及以下已测试geckoview内核及Crosswalk内核都会播放失败),其他方案请考虑tvbox 19 | 安卓7.0以上系统无需添加x5内核,可以去除,以提升运行速度及占用大小 20 | ## 已更新功能 21 | 今日节目单切换(遥控器菜单键呼出) 22 | 直播进度后退前进(遥控器左右键) 23 | 央视频vip源添加(目前发现问题是cctv特定频道有版权限制,央视频则没有) 24 | 25 | 26 | 27 | ## 计划更新 28 | 下版本计划添加 29 | 频道选择 30 | 31 | 最后感谢chatgpt的大力支持,让我3天学会Android入门 32 | 33 | ## 目前可看频道 34 | "CCTV-1 综合", 35 | "CCTV-2 财经", 36 | "CCTV-3 综艺", 37 | "CCTV-4 中文国际", 38 | "CCTV-5 体育", 39 | "CCTV-6 电影", 40 | "CCTV-7 军事农业", 41 | "CCTV-8 电视剧", 42 | "CCTV-9 纪录", 43 | "CCTV-10 科教", 44 | "CCTV-11 戏曲", 45 | "CCTV-12 社会与法", 46 | "CCTV-13 新闻", 47 | "CCTV-14 少儿", 48 | "CCTV-15 音乐", 49 | "CCTV-16 奥林匹克", 50 | "CCTV-17 农业农村", 51 | "CCTV-5+ 体育赛事", 52 | "CCTV 欧洲", 53 | "CCTV 美国", 54 | 55 | 央视频 56 | "CCTV4K", 57 | "CCTV1", 58 | "CCTV2", 59 | "CCTV3", 60 | "CCTV4", 61 | "CCTV5", 62 | "CCTV5+", 63 | "CCTV6", 64 | "CCTV7", 65 | "CCTV8", 66 | "CCTV9", 67 | "CCTV10", 68 | "CCTV11", 69 | "CCTV12", 70 | "CCTV13", 71 | "CCTV14", 72 | "CCTV15", 73 | "CCTV16-HD", 74 | "CCTV16(4K)", 75 | "CCTV17", 76 | "CGTN", 77 | "CGTN法语频道", 78 | "CGTN俄语频道", 79 | "CGTN阿拉伯语频道", 80 | "CGTN西班牙语频道", 81 | "CGTN外语纪录频道", 82 | "CCTV风云剧场频道", 83 | "CCTV第一剧场频道", 84 | "CCTV怀旧剧场频道", 85 | "CCTV世界地理频道", 86 | "CCTV风云音乐频道", 87 | "CCTV兵器科技频道", 88 | "CCTV高尔夫·网球频道", 89 | "CCTV女性时尚频道", 90 | "CCTV央视文化精品频道", 91 | "CCTV央视台球频道", 92 | "CCTV电视指南频道", 93 | "CCTV卫生健康频道", 94 | 95 | "北京卫视", 96 | "江苏卫视", 97 | "东方卫视", 98 | "浙江卫视", 99 | "湖南卫视", 100 | "湖北卫视", 101 | "广东卫视", 102 | "广西卫视", 103 | "黑龙江卫视", 104 | "海南卫视", 105 | "重庆卫视", 106 | "深圳卫视", 107 | "四川卫视", 108 | "河南卫视", 109 | "福建东南卫视", 110 | "贵州卫视", 111 | "江西卫视", 112 | "辽宁卫视", 113 | "安徽卫视", 114 | "河北卫视", 115 | "山东卫视" 116 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | namespace 'com.eanyatonic.cctvViewer' 7 | compileSdk 33 8 | sourceSets { 9 | main { 10 | assets.srcDirs = ['src/main/assets', 'src\\main\\assets'] 11 | } 12 | } 13 | defaultConfig { 14 | applicationId "com.eanyatonic.cctvViewer" 15 | minSdk 21 16 | targetSdk 33 17 | versionCode 1 18 | versionName "1.0" 19 | } 20 | 21 | buildTypes { 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | 28 | compileOptions { 29 | sourceCompatibility JavaVersion.VERSION_15 30 | targetCompatibility JavaVersion.VERSION_15 31 | } 32 | 33 | } 34 | 35 | dependencies { 36 | 37 | implementation 'androidx.leanback:leanback:1.0.0' 38 | implementation 'com.github.bumptech.glide:glide:4.16.0' 39 | implementation 'androidx.appcompat:appcompat:1.6.1' 40 | api 'com.tencent.tbs:tbssdk:44286' 41 | implementation 'androidx.recyclerview:recyclerview:1.3.2' 42 | // 添加 OkHttp 依赖 43 | implementation 'com.squareup.okhttp3:okhttp:4.11.0' 44 | } -------------------------------------------------------------------------------- /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 | -dontwarn dalvik.** 24 | -dontwarn com.tencent.smtt.** 25 | 26 | -keep class com.tencent.smtt.** { 27 | *; 28 | } 29 | 30 | -keep class com.tencent.tbs.** { 31 | *; 32 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 14 | 15 | 21 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 41 | 44 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/assets/046279_arm64v8a_x5.tbs.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zcodeoooo/CCTV_Viewer/2e99f56235e9beda50d7a6689b01ed610fe055b4/app/src/main/assets/046279_arm64v8a_x5.tbs.apk -------------------------------------------------------------------------------- /app/src/main/assets/046914_armeabi_x5.tbs.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zcodeoooo/CCTV_Viewer/2e99f56235e9beda50d7a6689b01ed610fe055b4/app/src/main/assets/046914_armeabi_x5.tbs.apk -------------------------------------------------------------------------------- /app/src/main/assets/js/backwardScript.js: -------------------------------------------------------------------------------- 1 | function simulate(element, eventName) { 2 | var options = extend(defaultOptions, arguments[2] || {}); 3 | var oEvent, eventType = null; 4 | 5 | for (var name in eventMatchers) { 6 | if (eventMatchers[name].test(eventName)) { 7 | eventType = name; 8 | break; 9 | } 10 | } 11 | 12 | if (!eventType) throw new SyntaxError('Only HTMLEvents and MouseEvents interfaces are supported'); 13 | 14 | if (document.createEvent) { 15 | oEvent = document.createEvent(eventType); 16 | if (eventType == 'HTMLEvents') { 17 | oEvent.initEvent(eventName, options.bubbles, options.cancelable); 18 | } else { 19 | oEvent.initMouseEvent(eventName, options.bubbles, options.cancelable, document.defaultView, options.button, options.pointerX, options.pointerY, options.pointerX, options.pointerY, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, element); 20 | } 21 | element.dispatchEvent(oEvent); 22 | } else { 23 | options.clientX = options.pointerX; 24 | options.clientY = options.pointerY; 25 | var evt = document.createEventObject(); 26 | oEvent = extend(evt, options); 27 | element.fireEvent('on' + eventName, oEvent); 28 | } 29 | return element; 30 | } 31 | 32 | function extend(destination, source) { 33 | for (var property in source) destination[property] = source[property]; 34 | return destination; 35 | } 36 | 37 | var eventMatchers = { 38 | 'HTMLEvents': /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/, 39 | 'MouseEvents': /^(?:click|dblclick|mouse(?:down|up|over|move|out))$/ 40 | } 41 | var defaultOptions = { 42 | pointerX: 0, 43 | pointerY: 0, 44 | button: 0, 45 | ctrlKey: false, 46 | altKey: false, 47 | shiftKey: false, 48 | metaKey: false, 49 | bubbles: true, 50 | cancelable: true 51 | } 52 | 53 | function triggerMouseEvent (node, eventType) { 54 | var clickEvent = document.createEvent ('MouseEvents'); 55 | clickEvent.initEvent (eventType, true, true); 56 | node.dispatchEvent (clickEvent); 57 | }; 58 | 59 | function mouseDragStart(node) { 60 | console.log("Starting drag..."); 61 | console.log(node.offsetTop); 62 | console.log(node.offsetLeft); 63 | triggerMouseEvent(node, "mousedown") 64 | } 65 | 66 | function mouseDragEnd(node){ 67 | console.log("Ending drag..."); 68 | const rect = node.getBoundingClientRect(); 69 | document.querySelector("#epg_right_shift_player").click() 70 | simulate(node, "mousemove" , {pointerX: rect.x-3 , pointerY: node.offsetTop}) 71 | simulate(node, "mouseup" , {pointerX: rect.x-3, pointerY: node.offsetTop}) 72 | } 73 | function sleep1(time) { 74 | time*=1000 75 | return new Promise(resolve => { 76 | setTimeout(() => { 77 | resolve(); 78 | }, time); 79 | }); 80 | } 81 | 82 | async function playback(){ 83 | if(document.querySelector("#play_or_pause_play_player").style.display==="none"){ 84 | document.querySelector("#play_or_plause_player").click() 85 | } 86 | await sleep1(3) 87 | const targetElement = document.querySelector("#timeshift_pointer_player") 88 | mouseDragStart(targetElement); 89 | mouseDragEnd(targetElement); 90 | }; 91 | 92 | playback() 93 | console.log('执行了回退'); -------------------------------------------------------------------------------- /app/src/main/assets/js/cctvOpen.js: -------------------------------------------------------------------------------- 1 | // 定义休眠函数 2 | function sleep(ms) { 3 | return new Promise(resolve => setTimeout(resolve, ms)); 4 | } 5 | 6 | // 页面加载完成后执行 JavaScript 脚本 7 | let interval = setInterval(async function executeScript() { 8 | console.log('页面加载完成!'); 9 | 10 | // 休眠 1000 毫秒(1秒) 11 | await sleep(1000); 12 | 13 | // 休眠 50 毫秒 14 | await sleep(50); 15 | 16 | console.log('点击分辨率按钮'); 17 | var elem = document.querySelector('#resolution_item_720_player'); 18 | elem.click(); 19 | 20 | // 休眠 50 毫秒 21 | await sleep(50); 22 | 23 | console.log('设置音量并点击音量按钮'); 24 | var btn = document.querySelector('#player_sound_btn_player'); 25 | btn.setAttribute('volume', 100); 26 | btn.click(); 27 | btn.click(); 28 | btn.click(); 29 | 30 | // 休眠 50 毫秒 31 | await sleep(50); 32 | 33 | console.log('点击全屏按钮'); 34 | var fullscreenBtn = document.querySelector('#player_pagefullscreen_yes_player'); 35 | fullscreenBtn.click(); 36 | clearInterval(interval); 37 | }, 3000); -------------------------------------------------------------------------------- /app/src/main/assets/js/forwardScript.js: -------------------------------------------------------------------------------- 1 | function simulate(element, eventName) { 2 | var options = extend(defaultOptions, arguments[2] || {}); 3 | var oEvent, eventType = null; 4 | 5 | for (var name in eventMatchers) { 6 | if (eventMatchers[name].test(eventName)) { 7 | eventType = name; 8 | break; 9 | } 10 | } 11 | 12 | if (!eventType) throw new SyntaxError('Only HTMLEvents and MouseEvents interfaces are supported'); 13 | 14 | if (document.createEvent) { 15 | oEvent = document.createEvent(eventType); 16 | if (eventType == 'HTMLEvents') { 17 | oEvent.initEvent(eventName, options.bubbles, options.cancelable); 18 | } else { 19 | oEvent.initMouseEvent(eventName, options.bubbles, options.cancelable, document.defaultView, options.button, options.pointerX, options.pointerY, options.pointerX, options.pointerY, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, element); 20 | } 21 | element.dispatchEvent(oEvent); 22 | } else { 23 | options.clientX = options.pointerX; 24 | options.clientY = options.pointerY; 25 | var evt = document.createEventObject(); 26 | oEvent = extend(evt, options); 27 | element.fireEvent('on' + eventName, oEvent); 28 | } 29 | return element; 30 | } 31 | 32 | function extend(destination, source) { 33 | for (var property in source) destination[property] = source[property]; 34 | return destination; 35 | } 36 | 37 | var eventMatchers = { 38 | 'HTMLEvents': /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/, 39 | 'MouseEvents': /^(?:click|dblclick|mouse(?:down|up|over|move|out))$/ 40 | } 41 | var defaultOptions = { 42 | pointerX: 0, 43 | pointerY: 0, 44 | button: 0, 45 | ctrlKey: false, 46 | altKey: false, 47 | shiftKey: false, 48 | metaKey: false, 49 | bubbles: true, 50 | cancelable: true 51 | } 52 | 53 | function triggerMouseEvent (node, eventType) { 54 | var clickEvent = document.createEvent ('MouseEvents'); 55 | clickEvent.initEvent (eventType, true, true); 56 | node.dispatchEvent (clickEvent); 57 | }; 58 | 59 | function mouseDragStart(node) { 60 | console.log("Starting drag..."); 61 | console.log(node.offsetTop); 62 | console.log(node.offsetLeft); 63 | triggerMouseEvent(node, "mousedown") 64 | } 65 | 66 | function mouseDragEnd(node){ 67 | console.log("Ending drag..."); 68 | const rect = node.getBoundingClientRect(); 69 | document.querySelector("#epg_right_shift_player").click() 70 | simulate(node, "mousemove" , {pointerX: rect.x+20 , pointerY: node.offsetTop}) 71 | simulate(node, "mouseup" , {pointerX: rect.x+20, pointerY: node.offsetTop}) 72 | } 73 | function sleep1(time) { 74 | time*=1000 75 | return new Promise(resolve => { 76 | setTimeout(() => { 77 | resolve(); 78 | }, time); 79 | }); 80 | } 81 | 82 | async function playback(){ 83 | if(document.querySelector("#play_or_pause_play_player").style.display==="none"){ 84 | document.querySelector("#play_or_plause_player").click() 85 | } 86 | await sleep1(3) 87 | const targetElement = document.querySelector("#timeshift_pointer_player") 88 | mouseDragStart(targetElement); 89 | mouseDragEnd(targetElement); 90 | }; 91 | 92 | playback() 93 | console.log('执行了前进'); -------------------------------------------------------------------------------- /app/src/main/assets/js/getEpgScript.js: -------------------------------------------------------------------------------- 1 | { 2 | // 定义休眠函数 3 | function sleep(ms) { 4 | return new Promise(resolve => setTimeout(resolve, ms)); 5 | } 6 | 7 | // 页面加载完成后执行 JavaScript 脚本 8 | let interval = setInterval(async function executeScript() { 9 | console.log('页面加载完成!'); 10 | 11 | // 休眠 1000 毫秒(1秒) 12 | await sleep(1000); 13 | 14 | // 休眠 50 毫秒 15 | await sleep(50); 16 | 17 | console.log('点击分辨率按钮'); 18 | var elem = document.querySelector('#resolution_item_720_player'); 19 | elem.click(); 20 | 21 | // 休眠 50 毫秒 22 | await sleep(50); 23 | 24 | console.log('设置音量并点击音量按钮'); 25 | var btn = document.querySelector('#player_sound_btn_player'); 26 | btn.setAttribute('volume', 100); 27 | btn.click(); 28 | btn.click(); 29 | btn.click(); 30 | 31 | // 休眠 50 毫秒 32 | await sleep(50); 33 | 34 | console.log('点击全屏按钮'); 35 | var fullscreenBtn = document.querySelector('#player_pagefullscreen_yes_player'); 36 | fullscreenBtn.click(); 37 | clearInterval(interval); 38 | }, 3000); 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/BrowseErrorActivity.java: -------------------------------------------------------------------------------- 1 | package com.eanyatonic.cctvViewer; 2 | 3 | import android.os.Bundle; 4 | import android.os.Handler; 5 | import android.os.Looper; 6 | import android.view.Gravity; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.FrameLayout; 11 | import android.widget.ProgressBar; 12 | 13 | import androidx.fragment.app.Fragment; 14 | import androidx.fragment.app.FragmentActivity; 15 | 16 | /* 17 | * BrowseErrorActivity shows how to use ErrorFragment 18 | */ 19 | public class BrowseErrorActivity extends FragmentActivity { 20 | private static final int TIMER_DELAY = 3000; 21 | private static final int SPINNER_WIDTH = 100; 22 | private static final int SPINNER_HEIGHT = 100; 23 | 24 | private ErrorFragment mErrorFragment; 25 | private SpinnerFragment mSpinnerFragment; 26 | 27 | /** 28 | * Called when the activity is first created. 29 | */ 30 | @Override 31 | public void onCreate(Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | setContentView(R.layout.activity_main); 34 | if (savedInstanceState == null) { 35 | getSupportFragmentManager().beginTransaction() 36 | .replace(R.id.main_browse_fragment, new MainFragment()) 37 | .commitNow(); 38 | } 39 | testError(); 40 | } 41 | 42 | private void testError() { 43 | mErrorFragment = new ErrorFragment(); 44 | getSupportFragmentManager() 45 | .beginTransaction() 46 | .add(R.id.main_browse_fragment, mErrorFragment) 47 | .commit(); 48 | 49 | mSpinnerFragment = new SpinnerFragment(); 50 | getSupportFragmentManager() 51 | .beginTransaction() 52 | .add(R.id.main_browse_fragment, mSpinnerFragment) 53 | .commit(); 54 | 55 | final Handler handler = new Handler(Looper.myLooper()); 56 | handler.postDelayed(new Runnable() { 57 | @Override 58 | public void run() { 59 | getSupportFragmentManager() 60 | .beginTransaction() 61 | .remove(mSpinnerFragment) 62 | .commit(); 63 | mErrorFragment.setErrorContent(); 64 | } 65 | }, TIMER_DELAY); 66 | } 67 | 68 | public static class SpinnerFragment extends Fragment { 69 | @Override 70 | public View onCreateView( 71 | LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 72 | ProgressBar progressBar = new ProgressBar(container.getContext()); 73 | if (container instanceof FrameLayout) { 74 | FrameLayout.LayoutParams layoutParams = 75 | new FrameLayout.LayoutParams(SPINNER_WIDTH, SPINNER_HEIGHT, Gravity.CENTER); 76 | progressBar.setLayoutParams(layoutParams); 77 | } 78 | return progressBar; 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/CardPresenter.java: -------------------------------------------------------------------------------- 1 | package com.eanyatonic.cctvViewer; 2 | 3 | import android.graphics.drawable.Drawable; 4 | 5 | import androidx.leanback.widget.ImageCardView; 6 | import androidx.leanback.widget.Presenter; 7 | import androidx.core.content.ContextCompat; 8 | 9 | import android.util.Log; 10 | import android.view.ViewGroup; 11 | 12 | import com.bumptech.glide.Glide; 13 | 14 | /* 15 | * A CardPresenter is used to generate Views and bind Objects to them on demand. 16 | * It contains an Image CardView 17 | */ 18 | public class CardPresenter extends Presenter { 19 | private static final String TAG = "CardPresenter"; 20 | 21 | private static final int CARD_WIDTH = 313; 22 | private static final int CARD_HEIGHT = 176; 23 | private static int sSelectedBackgroundColor; 24 | private static int sDefaultBackgroundColor; 25 | private Drawable mDefaultCardImage; 26 | 27 | private static void updateCardBackgroundColor(ImageCardView view, boolean selected) { 28 | int color = selected ? sSelectedBackgroundColor : sDefaultBackgroundColor; 29 | // Both background colors should be set because the view"s background is temporarily visible 30 | // during animations. 31 | view.setBackgroundColor(color); 32 | view.setInfoAreaBackgroundColor(color); 33 | } 34 | 35 | @Override 36 | public ViewHolder onCreateViewHolder(ViewGroup parent) { 37 | Log.d(TAG, "onCreateViewHolder"); 38 | 39 | sDefaultBackgroundColor = 40 | ContextCompat.getColor(parent.getContext(), R.color.default_background); 41 | sSelectedBackgroundColor = 42 | ContextCompat.getColor(parent.getContext(), R.color.selected_background); 43 | /* 44 | * This template uses a default image in res/drawable, but the general case for Android TV 45 | * will require your resources in xhdpi. For more information, see 46 | * https://developer.android.com/training/tv/start/layouts.html#density-resources 47 | */ 48 | mDefaultCardImage = ContextCompat.getDrawable(parent.getContext(), R.drawable.movie); 49 | 50 | ImageCardView cardView = 51 | new ImageCardView(parent.getContext()) { 52 | @Override 53 | public void setSelected(boolean selected) { 54 | updateCardBackgroundColor(this, selected); 55 | super.setSelected(selected); 56 | } 57 | }; 58 | 59 | cardView.setFocusable(true); 60 | cardView.setFocusableInTouchMode(true); 61 | updateCardBackgroundColor(cardView, false); 62 | return new ViewHolder(cardView); 63 | } 64 | 65 | @Override 66 | public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { 67 | Movie movie = (Movie) item; 68 | ImageCardView cardView = (ImageCardView) viewHolder.view; 69 | 70 | Log.d(TAG, "onBindViewHolder"); 71 | if (movie.getCardImageUrl() != null) { 72 | cardView.setTitleText(movie.getTitle()); 73 | cardView.setContentText(movie.getStudio()); 74 | cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT); 75 | Glide.with(viewHolder.view.getContext()) 76 | .load(movie.getCardImageUrl()) 77 | .centerCrop() 78 | .error(mDefaultCardImage) 79 | .into(cardView.getMainImageView()); 80 | } 81 | } 82 | 83 | @Override 84 | public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { 85 | Log.d(TAG, "onUnbindViewHolder"); 86 | ImageCardView cardView = (ImageCardView) viewHolder.view; 87 | // Remove references to images so that the garbage collector can free up memory 88 | cardView.setBadgeImage(null); 89 | cardView.setMainImage(null); 90 | } 91 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/ChannelAdapter.java: -------------------------------------------------------------------------------- 1 | package com.eanyatonic.cctvViewer; 2 | 3 | import android.util.Log; 4 | import android.view.KeyEvent; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.TextView; 9 | 10 | import androidx.core.content.ContextCompat; 11 | import androidx.recyclerview.widget.LinearLayoutManager; 12 | import androidx.recyclerview.widget.RecyclerView; 13 | 14 | import com.eanyatonic.cctvViewer.bean.EpgInfo; 15 | 16 | import java.util.List; 17 | 18 | public class ChannelAdapter extends RecyclerView.Adapter { 19 | 20 | private List channelList; 21 | private RecyclerView recyclerView; 22 | private com.tencent.smtt.sdk.WebView view; 23 | 24 | public ChannelAdapter(List channelList, RecyclerView recyclerView, com.tencent.smtt.sdk.WebView view) { 25 | this.channelList = channelList; 26 | this.recyclerView = recyclerView; 27 | this.view = view; 28 | } 29 | 30 | @Override 31 | public ChannelViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 32 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_channel, parent, false); 33 | return new ChannelViewHolder(view); 34 | } 35 | 36 | 37 | @Override 38 | public void onBindViewHolder(ChannelViewHolder holder, int position) { 39 | EpgInfo channel = channelList.get(position); 40 | holder.bind(channel); 41 | 42 | 43 | } 44 | 45 | 46 | @Override 47 | public int getItemCount() { 48 | return channelList.size(); 49 | } 50 | 51 | 52 | 53 | public class ChannelViewHolder extends RecyclerView.ViewHolder { 54 | public TextView channelNameTextView; 55 | public Integer pos = 0; 56 | 57 | 58 | public ChannelViewHolder(View itemView) { 59 | super(itemView); 60 | channelNameTextView = itemView.findViewById(R.id.channelNameTextView); 61 | recyclerView.requestFocus(); 62 | channelNameTextView.setOnClickListener(v -> { 63 | EpgInfo epgInfo = channelList.get(recyclerView.getLayoutManager().getPosition(v)); 64 | Log.d(epgInfo.getName(), epgInfo.getName() + "xxx"); 65 | view.evaluateJavascript("async function xx(){document.querySelector('#play_or_plause_player').click();await sleep(3000);" 66 | + "document.querySelector('#" + epgInfo.getId() + "')" + ".click();" + "}" + "xx()" 67 | , null); 68 | }); 69 | 70 | 71 | channelNameTextView.setOnFocusChangeListener((v, hasFocus) -> { 72 | if (hasFocus) { 73 | int position = recyclerView.getLayoutManager().getPosition(v); 74 | if (pos>=position){ 75 | pos = position; 76 | recyclerView.scrollToPosition(position-1); 77 | }else { 78 | pos = position; 79 | recyclerView.scrollToPosition(position+1); 80 | } 81 | channelNameTextView.setBackgroundResource(R.drawable.channel_background_unselected); 82 | } else { 83 | channelNameTextView.setBackgroundResource(R.drawable.channel_background_selected); 84 | } 85 | }); 86 | 87 | 88 | } 89 | 90 | 91 | public void bind(EpgInfo channel) { 92 | channelNameTextView.setText(channel.getName()); 93 | } 94 | 95 | } 96 | 97 | 98 | 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/DetailsActivity.java: -------------------------------------------------------------------------------- 1 | package com.eanyatonic.cctvViewer; 2 | 3 | import android.os.Bundle; 4 | 5 | import androidx.fragment.app.FragmentActivity; 6 | 7 | /* 8 | * Details activity class that loads LeanbackDetailsFragment class 9 | */ 10 | public class DetailsActivity extends FragmentActivity { 11 | public static final String SHARED_ELEMENT_NAME = "hero"; 12 | public static final String MOVIE = "Movie"; 13 | 14 | /** 15 | * Called when the activity is first created. 16 | */ 17 | @Override 18 | public void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_details); 21 | if (savedInstanceState == null) { 22 | getSupportFragmentManager().beginTransaction() 23 | .replace(R.id.details_fragment, new VideoDetailsFragment()) 24 | .commitNow(); 25 | } 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/DetailsDescriptionPresenter.java: -------------------------------------------------------------------------------- 1 | package com.eanyatonic.cctvViewer; 2 | 3 | import androidx.leanback.widget.AbstractDetailsDescriptionPresenter; 4 | 5 | public class DetailsDescriptionPresenter extends AbstractDetailsDescriptionPresenter { 6 | 7 | @Override 8 | protected void onBindDescription(ViewHolder viewHolder, Object item) { 9 | Movie movie = (Movie) item; 10 | 11 | if (movie != null) { 12 | viewHolder.getTitle().setText(movie.getTitle()); 13 | viewHolder.getSubtitle().setText(movie.getStudio()); 14 | viewHolder.getBody().setText(movie.getDescription()); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/ErrorFragment.java: -------------------------------------------------------------------------------- 1 | package com.eanyatonic.cctvViewer; 2 | 3 | import android.os.Bundle; 4 | import android.util.Log; 5 | import android.view.View; 6 | 7 | import androidx.core.content.ContextCompat; 8 | import androidx.leanback.app.ErrorSupportFragment; 9 | 10 | /* 11 | * This class demonstrates how to extend ErrorSupportFragment. 12 | */ 13 | public class ErrorFragment extends ErrorSupportFragment { 14 | private static final String TAG = "ErrorFragment"; 15 | private static final boolean TRANSLUCENT = true; 16 | 17 | @Override 18 | public void onCreate(Bundle savedInstanceState) { 19 | Log.d(TAG, "onCreate"); 20 | super.onCreate(savedInstanceState); 21 | setTitle(getResources().getString(R.string.app_name)); 22 | } 23 | 24 | void setErrorContent() { 25 | setImageDrawable(ContextCompat.getDrawable(getActivity(), androidx.leanback.R.drawable.lb_ic_sad_cloud)); 26 | setMessage(getResources().getString(R.string.error_fragment_message)); 27 | setDefaultBackground(TRANSLUCENT); 28 | 29 | setButtonText(getResources().getString(R.string.dismiss_error)); 30 | setButtonClickListener( 31 | new View.OnClickListener() { 32 | @Override 33 | public void onClick(View arg0) { 34 | getFragmentManager().beginTransaction().remove(ErrorFragment.this).commit(); 35 | } 36 | }); 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.eanyatonic.cctvViewer; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.SharedPreferences; 6 | import android.content.pm.ActivityInfo; 7 | import android.graphics.Color; 8 | import android.os.Build; 9 | import android.os.Bundle; 10 | import android.os.Handler; 11 | import android.os.SystemClock; 12 | import android.util.DisplayMetrics; 13 | import android.util.Log; 14 | import android.view.KeyEvent; 15 | import android.view.MotionEvent; 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | import android.view.inputmethod.InputMethodManager; 19 | import android.webkit.JavascriptInterface; 20 | import android.widget.FrameLayout; 21 | import android.widget.RelativeLayout; 22 | import android.widget.TextView; 23 | import android.widget.Toast; 24 | 25 | import androidx.appcompat.app.AppCompatActivity; 26 | import androidx.recyclerview.widget.LinearLayoutManager; 27 | import androidx.recyclerview.widget.RecyclerView; 28 | 29 | import com.eanyatonic.cctvViewer.bean.EpgInfo; 30 | import com.eanyatonic.cctvViewer.tools.FileTool; 31 | import com.eanyatonic.cctvViewer.tools.FileUtils; 32 | import com.eanyatonic.cctvViewer.tools.SysTool; 33 | import com.tencent.smtt.export.external.TbsCoreSettings; 34 | import com.tencent.smtt.export.external.interfaces.IX5WebChromeClient; 35 | import com.tencent.smtt.export.external.interfaces.SslError; 36 | import com.tencent.smtt.export.external.interfaces.SslErrorHandler; 37 | import com.tencent.smtt.sdk.CookieManager; 38 | import com.tencent.smtt.sdk.CookieSyncManager; 39 | import com.tencent.smtt.sdk.QbSdk; 40 | import com.tencent.smtt.sdk.WebChromeClient; 41 | import com.tencent.smtt.sdk.WebSettings; 42 | import com.tencent.smtt.sdk.WebView; 43 | import com.tencent.smtt.sdk.WebViewClient; 44 | 45 | import org.json.JSONArray; 46 | import org.json.JSONException; 47 | import org.json.JSONObject; 48 | 49 | import java.io.IOException; 50 | import java.util.ArrayList; 51 | import java.util.HashMap; 52 | import java.util.List; 53 | import java.util.concurrent.Executor; 54 | import java.util.concurrent.Executors; 55 | 56 | import okhttp3.HttpUrl; 57 | import okhttp3.OkHttpClient; 58 | import okhttp3.Request; 59 | import okhttp3.Response; 60 | 61 | public class MainActivity extends AppCompatActivity { 62 | 63 | public FileTool filetool; 64 | private RecyclerView recyclerView; 65 | private View mCustomView; 66 | private FrameLayout mFrameLayout; 67 | private IX5WebChromeClient.CustomViewCallback mCustomViewCallback; 68 | private com.tencent.smtt.sdk.WebView webView; // 导入 X5 WebView 69 | private com.tencent.smtt.sdk.WebView cctvFinishedView; 70 | 71 | private String aach; 72 | private ChannelAdapter channelAdapter; 73 | private final String[] liveUrls = { 74 | "https://tv.cctv.com/live/cctv1/", 75 | "https://tv.cctv.com/live/cctv2/", 76 | "https://tv.cctv.com/live/cctv3/", 77 | "https://tv.cctv.com/live/cctv4/", 78 | "https://tv.cctv.com/live/cctv5/", 79 | "https://tv.cctv.com/live/cctv6/", 80 | "https://tv.cctv.com/live/cctv7/", 81 | "https://tv.cctv.com/live/cctv8/", 82 | "https://tv.cctv.com/live/cctvjilu", 83 | "https://tv.cctv.com/live/cctv10/", 84 | "https://tv.cctv.com/live/cctv11/", 85 | "https://tv.cctv.com/live/cctv12/", 86 | "https://tv.cctv.com/live/cctv13/", 87 | "https://tv.cctv.com/live/cctvchild", 88 | "https://tv.cctv.com/live/cctv15/", 89 | "https://tv.cctv.com/live/cctv16/", 90 | "https://tv.cctv.com/live/cctv17/", 91 | "https://tv.cctv.com/live/cctv5plus/", 92 | "https://tv.cctv.com/live/cctveurope", 93 | "https://tv.cctv.com/live/cctvamerica/", 94 | 95 | "https://www.yangshipin.cn/#/tv/home?pid=600002264", 96 | "https://www.yangshipin.cn/#/tv/home?pid=600001859", 97 | "https://www.yangshipin.cn/#/tv/home?pid=600001800", 98 | "https://www.yangshipin.cn/#/tv/home?pid=600001801", 99 | "https://www.yangshipin.cn/#/tv/home?pid=600001814", 100 | "https://www.yangshipin.cn/#/tv/home?pid=600001818", 101 | "https://www.yangshipin.cn/#/tv/home?pid=600001817", 102 | "https://www.yangshipin.cn/#/tv/home?pid=600001802", 103 | "https://www.yangshipin.cn/#/tv/home?pid=600004092", 104 | "https://www.yangshipin.cn/#/tv/home?pid=600001803", 105 | "https://www.yangshipin.cn/#/tv/home?pid=600004078", 106 | "https://www.yangshipin.cn/#/tv/home?pid=600001805", 107 | "https://www.yangshipin.cn/#/tv/home?pid=600001806", 108 | "https://www.yangshipin.cn/#/tv/home?pid=600001807", 109 | "https://www.yangshipin.cn/#/tv/home?pid=600001811", 110 | "https://www.yangshipin.cn/#/tv/home?pid=600001809", 111 | "https://www.yangshipin.cn/#/tv/home?pid=600001815", 112 | "https://www.yangshipin.cn/#/tv/home?pid=600098637", 113 | "https://www.yangshipin.cn/#/tv/home?pid=600099502", 114 | "https://www.yangshipin.cn/#/tv/home?pid=600001810", 115 | "https://www.yangshipin.cn/#/tv/home?pid=600014550", 116 | "https://www.yangshipin.cn/#/tv/home?pid=600084704", 117 | "https://www.yangshipin.cn/#/tv/home?pid=600084758", 118 | "https://www.yangshipin.cn/#/tv/home?pid=600084782", 119 | "https://www.yangshipin.cn/#/tv/home?pid=600084744", 120 | "https://www.yangshipin.cn/#/tv/home?pid=600084781", 121 | "https://www.yangshipin.cn/#/tv/home?pid=600099658", 122 | "https://www.yangshipin.cn/#/tv/home?pid=600099655", 123 | "https://www.yangshipin.cn/#/tv/home?pid=600099620", 124 | "https://www.yangshipin.cn/#/tv/home?pid=600099637", 125 | "https://www.yangshipin.cn/#/tv/home?pid=600099660", 126 | "https://www.yangshipin.cn/#/tv/home?pid=600099649", 127 | "https://www.yangshipin.cn/#/tv/home?pid=600099636", 128 | "https://www.yangshipin.cn/#/tv/home?pid=600099659", 129 | "https://www.yangshipin.cn/#/tv/home?pid=600099650", 130 | "https://www.yangshipin.cn/#/tv/home?pid=600099653", 131 | "https://www.yangshipin.cn/#/tv/home?pid=600099652", 132 | "https://www.yangshipin.cn/#/tv/home?pid=600099656", 133 | "https://www.yangshipin.cn/#/tv/home?pid=600099651", 134 | "https://www.yangshipin.cn/#/tv/home?pid=600002309", 135 | "https://www.yangshipin.cn/#/tv/home?pid=600002521", 136 | "https://www.yangshipin.cn/#/tv/home?pid=600002483", 137 | "https://www.yangshipin.cn/#/tv/home?pid=600002520", 138 | "https://www.yangshipin.cn/#/tv/home?pid=600002475", 139 | "https://www.yangshipin.cn/#/tv/home?pid=600002508", 140 | "https://www.yangshipin.cn/#/tv/home?pid=600002485", 141 | "https://www.yangshipin.cn/#/tv/home?pid=600002509", 142 | "https://www.yangshipin.cn/#/tv/home?pid=600002498", 143 | "https://www.yangshipin.cn/#/tv/home?pid=600002506", 144 | "https://www.yangshipin.cn/#/tv/home?pid=600002531", 145 | "https://www.yangshipin.cn/#/tv/home?pid=600002481", 146 | "https://www.yangshipin.cn/#/tv/home?pid=600002516", 147 | "https://www.yangshipin.cn/#/tv/home?pid=600002525", 148 | "https://www.yangshipin.cn/#/tv/home?pid=600002484", 149 | "https://www.yangshipin.cn/#/tv/home?pid=600002490", 150 | "https://www.yangshipin.cn/#/tv/home?pid=600002503", 151 | "https://www.yangshipin.cn/#/tv/home?pid=600002505", 152 | "https://www.yangshipin.cn/#/tv/home?pid=600002532", 153 | "https://www.yangshipin.cn/#/tv/home?pid=600002493", 154 | "https://www.yangshipin.cn/#/tv/home?pid=600002513", 155 | }; 156 | 157 | 158 | /** 159 | * for (let index = 0; index < document.querySelector(".tv-main-con-r-list-left").parentElement.parentElement.parentElement.parentElement.parentElement.parentElement.__vue__._data.tabB.length; index++) { 160 | * console.log(document.querySelector(".tv-main-con-r-list-left").parentElement.parentElement.parentElement.parentElement.parentElement.parentElement.__vue__._data.tabB[index].pid) 161 | * } 162 | */ 163 | 164 | private String[] channelNames = { 165 | "CCTV-1 综合", 166 | "CCTV-2 财经", 167 | "CCTV-3 综艺", 168 | "CCTV-4 中文国际", 169 | "CCTV-5 体育", 170 | "CCTV-6 电影", 171 | "CCTV-7 军事农业", 172 | "CCTV-8 电视剧", 173 | "CCTV-9 纪录", 174 | "CCTV-10 科教", 175 | "CCTV-11 戏曲", 176 | "CCTV-12 社会与法", 177 | "CCTV-13 新闻", 178 | "CCTV-14 少儿", 179 | "CCTV-15 音乐", 180 | "CCTV-16 奥林匹克", 181 | "CCTV-17 农业农村", 182 | "CCTV-5+ 体育赛事", 183 | "CCTV 欧洲", 184 | "CCTV 美国", 185 | 186 | "CCTV4K", 187 | "CCTV1", 188 | "CCTV2", 189 | "CCTV3", 190 | "CCTV4", 191 | "CCTV5", 192 | "CCTV5+", 193 | "CCTV6", 194 | "CCTV7", 195 | "CCTV8", 196 | "CCTV9", 197 | "CCTV10", 198 | "CCTV11", 199 | "CCTV12", 200 | "CCTV13", 201 | "CCTV14", 202 | "CCTV15", 203 | "CCTV16-HD", 204 | "CCTV16(4K)", 205 | "CCTV17", 206 | "CGTN", 207 | "CGTN法语频道", 208 | "CGTN俄语频道", 209 | "CGTN阿拉伯语频道", 210 | "CGTN西班牙语频道", 211 | "CGTN外语纪录频道", 212 | "CCTV风云剧场频道", 213 | "CCTV第一剧场频道", 214 | "CCTV怀旧剧场频道", 215 | "CCTV世界地理频道", 216 | "CCTV风云音乐频道", 217 | "CCTV兵器科技频道", 218 | "CCTV风云足球频道", 219 | "CCTV高尔夫·网球频道", 220 | "CCTV女性时尚频道", 221 | "CCTV央视文化精品频道", 222 | "CCTV央视台球频道", 223 | "CCTV电视指南频道", 224 | "CCTV卫生健康频道", 225 | "北京卫视", 226 | "江苏卫视", 227 | "东方卫视", 228 | "浙江卫视", 229 | "湖南卫视", 230 | "湖北卫视", 231 | "广东卫视", 232 | "广西卫视", 233 | "黑龙江卫视", 234 | "海南卫视", 235 | "重庆卫视", 236 | "深圳卫视", 237 | "四川卫视", 238 | "河南卫视", 239 | "福建东南卫视", 240 | "贵州卫视", 241 | "江西卫视", 242 | "辽宁卫视", 243 | "安徽卫视", 244 | "河北卫视", 245 | "山东卫视" 246 | }; 247 | 248 | 249 | 250 | private int currentLiveIndex; 251 | 252 | private static final String PREF_NAME = "MyPreferences"; 253 | private static final String PREF_KEY_LIVE_INDEX = "currentLiveIndex"; 254 | 255 | private boolean doubleBackToExitPressedOnce = false; 256 | 257 | private final StringBuilder digitBuffer = new StringBuilder(); // 用于缓存按下的数字键 258 | private static final long DIGIT_TIMEOUT = 3000; // 超时时间(毫秒) 259 | 260 | private TextView inputTextView; // 用于显示正在输入的数字的 TextView 261 | 262 | // 初始化透明的View 263 | private View loadingOverlay; 264 | 265 | // 频道显示view 266 | private TextView overlayTextView; 267 | 268 | private String info = ""; 269 | 270 | public List epgList = new ArrayList<>(); 271 | 272 | 273 | private String getYSPToken() { 274 | // 在需要发送请求的方法中添加以下代码 275 | OkHttpClient client = new OkHttpClient(); 276 | // 构建请求 URL 277 | HttpUrl.Builder urlBuilder = HttpUrl.parse("https://lyrics.run/my-tv/v1/info").newBuilder(); 278 | String url = urlBuilder.build().toString(); 279 | 280 | Request request = new Request.Builder() 281 | .url(url) 282 | .build(); 283 | try (Response response = client.newCall(request).execute()) { 284 | if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); 285 | 286 | // 获取 JSON 字符串并解析为对象 287 | String json = response.body().string(); 288 | JSONObject jsonObject = new JSONObject(json); 289 | 290 | // 根据 JSON 的层级结构,逐级提取数据 291 | return jsonObject.getJSONObject("data").getString("token"); 292 | 293 | } catch (JSONException | IOException e) { 294 | throw new RuntimeException(e); 295 | } 296 | } 297 | 298 | 299 | 300 | @Override 301 | protected void onCreate(Bundle savedInstanceState) { 302 | Log.d("xxx",liveUrls.length+"xxxx"+channelNames.length); 303 | super.onCreate(savedInstanceState); 304 | setContentView(R.layout.activity_main); 305 | mFrameLayout = findViewById(R.id.flVideoContainer); 306 | // X5WebView初始化 307 | initX5WebView(); 308 | Toast.makeText(MainActivity.this, "x5内核状态" + QbSdk.canLoadX5(getApplicationContext()) + "内核架构" + SysTool.showSysAach() + ",若无法播放请重启应用", Toast.LENGTH_SHORT).show(); 309 | // 初始化 WebView 310 | webView = findViewById(R.id.webView); 311 | // 初始化显示正在输入的数字的 TextView 312 | inputTextView = findViewById(R.id.inputTextView); 313 | // 初始化 loadingOverlay 314 | loadingOverlay = findViewById(R.id.loadingOverlay); 315 | // 初始化 overlayTextView 316 | overlayTextView = findViewById(R.id.overlayTextView); 317 | 318 | // 加载上次保存的位置 319 | loadLastLiveIndex(); 320 | // 添加自动化调试 321 | // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 322 | // WebView.setWebContentsDebuggingEnabled(true); 323 | // Log.d("remote debug", "远程调试"); 324 | // } 325 | // 配置 WebView 设置 326 | // filetool = new FileTool(this); 327 | // String backwardScript =filetool.readFileContent("js/backwardScript.js"); 328 | // 329 | // String forwardScript =filetool.readFileContent("js/forwardScript.js"); 330 | // 331 | // String cctvOpenScript =filetool.readFileContent("js/getEpgScript.js"); 332 | webView.addJavascriptInterface(this, "bridge"); 333 | WebSettings webSettings = webView.getSettings(); 334 | webSettings.setJavaScriptEnabled(true); 335 | webSettings.setDomStorageEnabled(true); 336 | // 添加自动播放视频 337 | webSettings.setMediaPlaybackRequiresUserGesture(false); 338 | webSettings.setDatabaseEnabled(true); 339 | webSettings.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"); 340 | webSettings.setUseWideViewPort(true); 341 | // 启用 JavaScript 自动点击功能 342 | webSettings.setJavaScriptCanOpenWindowsAutomatically(true); 343 | webSettings.setMixedContentMode(WebSettings.LOAD_NORMAL); 344 | 345 | 346 | // 设置 WebViewClient 和 WebChromeClient 347 | webView.setWebViewClient(new WebViewClient() { 348 | 349 | @Override 350 | public void onReceivedSslError(WebView webView, SslErrorHandler handler, SslError error) { 351 | handler.proceed(); // 忽略 SSL 错误 352 | } 353 | 354 | // 设置 WebViewClient,监听页面加载完成事件 355 | @Override 356 | public void onPageFinished(WebView view, String url) { 357 | cctvFinishedView = view; 358 | info = ""; 359 | if (url.contains("cctv.com")){ 360 | // 获取节目预告和当前节目 361 | view.evaluateJavascript("document.querySelector('#jiemu > li.cur.act').innerText", value -> { 362 | // 处理获取到的元素值 363 | if (!value.equals("null") && !value.isEmpty()) { 364 | String elementValueNow = value.replace("\"", ""); // 去掉可能的引号 365 | info += elementValueNow + "\n"; 366 | } 367 | }); 368 | view.evaluateJavascript("document.querySelector('#jiemu > li:nth-child(4)').innerText", value -> { 369 | // 处理获取到的元素值 370 | if (!value.equals("null") && !value.isEmpty()) { 371 | String elementValueNext = value.replace("\"", ""); // 去掉可能的引号 372 | info += elementValueNext; 373 | } 374 | }); 375 | 376 | view.evaluateJavascript(""" 377 | { 378 | function sleep(ms) { 379 | return new Promise(resolve => setTimeout(resolve, ms)); 380 | } 381 | 382 | let interval = setInterval(async function () { 383 | console.log('页面加载完成!'); 384 | // 休眠 1000 毫秒(1秒) 385 | await sleep(1000); 386 | // 休眠 50 毫秒 387 | await sleep(50); 388 | console.log('获取节目单'); 389 | var epg_list = [ 390 | { name: "", id: "" }, 391 | ]; 392 | 393 | var epg_child = document.querySelector("#epg_player").childNodes; 394 | for (let index = 0; index < epg_child.length; index++) { 395 | if (index % 2 == 1) { 396 | epg_list.push({ name: epg_child[index].innerText, id: epg_child[index].id }); 397 | } 398 | } 399 | let filteredArray = epg_list.map(obj => { 400 | Object.keys(obj).forEach(key => obj[key] === '' || obj[key] === null ? delete obj[key] : ''); 401 | return obj; 402 | }).filter(obj => Object.keys(obj).length > 0); 403 | console.log(JSON.stringify(filteredArray)); 404 | clearInterval(interval); 405 | bridge.setCctvEpgInfo(JSON.stringify(filteredArray)); 406 | }, 3000); 407 | } 408 | """, null); 409 | 410 | view.evaluateJavascript(""" 411 | { 412 | function sleep(ms) { 413 | return new Promise(resolve => setTimeout(resolve, ms)); 414 | } 415 | 416 | // 页面加载完成后执行 JavaScript 脚本 417 | let interval = setInterval(async function executeScript() { 418 | console.log('页面加载完成!'); 419 | 420 | // 休眠 1000 毫秒(1秒) 421 | await sleep(1000); 422 | 423 | // 休眠 50 毫秒 424 | await sleep(50); 425 | 426 | console.log('点击分辨率按钮'); 427 | if(document.querySelector('#resolution_item_720_player')===null){ 428 | var elem = document.querySelector("#resolution_item_480_player") 429 | elem.click(); 430 | }else{ 431 | var elem = document.querySelector('#resolution_item_720_player'); 432 | elem.click(); 433 | } 434 | 435 | // 休眠 50 毫秒 436 | await sleep(50); 437 | 438 | console.log('设置音量并点击音量按钮'); 439 | var btn = document.querySelector('#player_sound_btn_player'); 440 | btn.setAttribute('volume', 100); 441 | btn.click(); 442 | btn.click(); 443 | btn.click(); 444 | document.querySelector("#player_sound_player").style.display = 'none' 445 | 446 | // 休眠 50 毫秒 447 | await sleep(50); 448 | 449 | console.log('点击全屏按钮'); 450 | var fullscreenBtn = document.querySelector('#player_pagefullscreen_yes_player'); 451 | fullscreenBtn.click(); 452 | clearInterval(interval); 453 | }, 3000); 454 | } 455 | """, null); 456 | } else if (url.contains("yangshipin")) { 457 | view.evaluateJavascript(""" 458 | { 459 | function sleep(ms) { 460 | return new Promise(resolve => setTimeout(resolve, ms)); 461 | } 462 | 463 | // 页面加载完成后执行 JavaScript 脚本 464 | let interval = setInterval(async function () { 465 | await sleep(1050); 466 | var cur; 467 | if(document.querySelector('.tv-main-con-r-list-left-imgb.tvSelect')===null){ 468 | // cctv频道当前频道查询 469 | cur = document.querySelector(".tv-main-con-r-list-left-imga.tvSelect").innerText.replace(/[\\s\\n]/g, '') 470 | }else{ 471 | // 地方频道当前频道查询 472 | cur = document.querySelector('.tv-main-con-r-list-left-imgb.tvSelect').innerText.replace(/[\\s\\n]/g, '') 473 | } 474 | console.log(cur) 475 | clearInterval(interval) 476 | }, 3000); 477 | } 478 | """,null); 479 | 480 | 481 | 482 | view.evaluateJavascript(""" 483 | { 484 | function sleep(ms) { 485 | return new Promise(resolve => setTimeout(resolve, ms)); 486 | } 487 | let interval = setInterval(async function () { 488 | await sleep(1050); 489 | console.log('设置音量并点击音量按钮'); 490 | document.querySelector(".container").__vue__.volume = 1 491 | await sleep(50); 492 | if(document.querySelector(".voice.on").style.display === 'none'){ 493 | await sleep(50); 494 | document.querySelector(".container").__vue__.changeVolume() 495 | } 496 | console.log('点击ysp全屏按钮'); 497 | document.querySelector(".videoFull").click() 498 | clearInterval(interval); 499 | }, 3000); 500 | } 501 | """, null); 502 | } 503 | 504 | new Handler().postDelayed(() -> { 505 | loadingOverlay.setVisibility(View.GONE); 506 | showOverlay(channelNames[currentLiveIndex] + "\n" + info); 507 | channelAdapter.notifyDataSetChanged(); 508 | }, 3000); 509 | 510 | } 511 | 512 | }); 513 | // 设置 WebView 客户端 514 | webView.setWebChromeClient(new WebChromeClient() { 515 | @Override 516 | public void onShowCustomView(View view, IX5WebChromeClient.CustomViewCallback callback) { 517 | Log.d("onShowCustomView", "onShowCustomView"); 518 | super.onShowCustomView(view, callback); 519 | if (mCustomView != null) { 520 | callback.onCustomViewHidden(); 521 | return; 522 | } 523 | mCustomView = view; 524 | mFrameLayout.addView(mCustomView); 525 | mCustomViewCallback = callback; 526 | webView.setVisibility(View.GONE); 527 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 528 | } 529 | 530 | @Override 531 | public void onHideCustomView() { 532 | Log.d("onHideCustomView", "onHideCustomView"); 533 | webView.setVisibility(View.VISIBLE); 534 | if (mCustomView == null) { 535 | return; 536 | } 537 | mCustomView.setVisibility(View.GONE); 538 | mFrameLayout.removeView(mCustomView); 539 | mCustomViewCallback.onCustomViewHidden(); 540 | mCustomView = null; 541 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 542 | super.onHideCustomView(); 543 | } 544 | 545 | 546 | }); 547 | 548 | 549 | // 禁用缩放 550 | webSettings.setSupportZoom(false); 551 | webSettings.setBuiltInZoomControls(false); 552 | webSettings.setDisplayZoomControls(false); 553 | 554 | // 在 Android TV 上,需要禁用焦点自动导航 555 | webView.setFocusable(false); 556 | 557 | 558 | // 加载初始网页 559 | webView.setBackgroundColor(Color.TRANSPARENT); 560 | webView.setLayoutParams(new RelativeLayout.LayoutParams( 561 | RelativeLayout.LayoutParams.MATCH_PARENT, 562 | RelativeLayout.LayoutParams.MATCH_PARENT)); 563 | 564 | recyclerView = findViewById(R.id.recyclerView); 565 | LinearLayoutManager linearLayoutManager = new LinearLayoutManager(MainActivity.this, LinearLayoutManager.VERTICAL, false); 566 | recyclerView.setLayoutManager(linearLayoutManager); 567 | recyclerView.setItemAnimator(null); 568 | epgList.add(new EpgInfo("暂无节目单", "暂无节目单")); 569 | channelAdapter = new ChannelAdapter(epgList, recyclerView, webView); 570 | recyclerView.setAdapter(channelAdapter); 571 | 572 | loadLiveUrl(); 573 | 574 | // new Handler().postDelayed(() -> { 575 | // RelativeLayout mainView = findViewById(R.id.main_browse_fragment); 576 | // if(mainView != null) { 577 | // mainView.removeView(cctvFinishedView); 578 | // } 579 | // mFrameLayout.addView(cctvFinishedView); 580 | // }, 6000); 581 | 582 | startPeriodicTask(); 583 | 584 | } 585 | private final Handler handler = new Handler(); 586 | // 启动自动播放定时任务 587 | private void startPeriodicTask() { 588 | // 使用 postDelayed 方法设置定时任务 589 | handler.postDelayed(periodicTask, 5000); // 2000 毫秒,即 2 秒钟 590 | } 591 | 592 | // 定时任务具体操作 593 | private final Runnable periodicTask = new Runnable() { 594 | @Override 595 | public void run() { 596 | // 获取 div 元素的 display 属性,并执行相应的操作 597 | getDivDisplayPropertyAndDoSimulateTouch(); 598 | 599 | // 完成后再次调度定时任务 600 | handler.postDelayed(this, 5000); // 2000 毫秒,即 2 秒钟 601 | } 602 | }; 603 | 604 | // 获取 div 元素的 display 属性并执行相应的操作 605 | private void getDivDisplayPropertyAndDoSimulateTouch() { 606 | if (webView != null) { 607 | if(currentLiveIndex>=getCCTVHeadOffset()&¤tLiveIndex<=getCCTVTailOffset()){ 608 | webView.evaluateJavascript("document.getElementById('play_or_pause_play_player').style.display", value -> { 609 | // 处理获取到的 display 属性值 610 | if (value.equals("\"block\"")) { 611 | // 执行点击操作 612 | simulateTouch(webView, 0.5f, 0.5f); 613 | } 614 | }); 615 | } else if (currentLiveIndex>=getYSPHeadOffset()&¤tLiveIndex<=getYSPTailOffset()){ 616 | String scriptPlay = 617 | """ 618 | if(!document.querySelector(".container").__vue__.playStatus.isPaused){ 619 | document.querySelector(".container").__vue__.togglePlay() 620 | } 621 | """; 622 | webView.evaluateJavascript(scriptPlay, null); 623 | } 624 | } 625 | } 626 | private void initX5WebView() { 627 | // 在调用TBS初始化、创建WebView之前进行如下配置2 628 | boolean canLoadX5 = QbSdk.canLoadX5(getApplicationContext()); 629 | if (!canLoadX5) { 630 | tbsInstall(); 631 | } 632 | 633 | } 634 | 635 | 636 | private void tbsInstall() { 637 | HashMap map = new HashMap<>(2); 638 | map.put(TbsCoreSettings.TBS_SETTINGS_USE_SPEEDY_CLASSLOADER, true); 639 | map.put(TbsCoreSettings.TBS_SETTINGS_USE_DEXLOADER_SERVICE, true); 640 | QbSdk.initTbsSettings(map); 641 | aach = SysTool.showSysAach(); 642 | if (aach.contains("arm64")) { 643 | FileUtils.copyAssets(getApplicationContext(), "046279_arm64v8a_x5.tbs.apk", FileUtils.getTBSFileDir(getApplicationContext()).getPath() + "/046279_arm64v8a_x5.tbs.apk"); 644 | QbSdk.reset(getApplicationContext()); 645 | QbSdk.installLocalTbsCore(getApplicationContext(), 46279, FileUtils.getTBSFileDir(getApplicationContext()).getPath() + "/046279_arm64v8a_x5.tbs.apk"); 646 | } else if (aach.contains("armeabi")) { 647 | FileUtils.copyAssets(getApplicationContext(), "046914_armeabi_x5.tbs.apk", FileUtils.getTBSFileDir(getApplicationContext()).getPath() + "/046914_armeabi_x5.tbs.apk"); 648 | QbSdk.reset(getApplicationContext()); 649 | QbSdk.installLocalTbsCore(getApplicationContext(), 46914, FileUtils.getTBSFileDir(getApplicationContext()).getPath() + "/046914_armeabi_x5.tbs.apk"); 650 | } 651 | 652 | Log.e("canloadX5", "canLoadX5: " + QbSdk.canLoadX5(getApplicationContext()) + "|TbsVersion:" + QbSdk.getTbsVersion(getApplicationContext())); 653 | } 654 | 655 | @Override 656 | public boolean dispatchKeyEvent(KeyEvent event) { 657 | if (!recyclerView.hasFocus() && event.getAction() == KeyEvent.ACTION_DOWN) { 658 | cctvFinishedView.evaluateJavascript( 659 | """ 660 | function simulate(element, eventName) { 661 | var options = extend(defaultOptions, arguments[2] || {}); 662 | var oEvent, eventType = null; 663 | 664 | for (var name in eventMatchers) { 665 | if (eventMatchers[name].test(eventName)) { 666 | eventType = name; 667 | break; 668 | } 669 | } 670 | 671 | if (!eventType) throw new SyntaxError('Only HTMLEvents and MouseEvents interfaces are supported'); 672 | 673 | if (document.createEvent) { 674 | oEvent = document.createEvent(eventType); 675 | if (eventType == 'HTMLEvents') { 676 | oEvent.initEvent(eventName, options.bubbles, options.cancelable); 677 | } else { 678 | oEvent.initMouseEvent(eventName, options.bubbles, options.cancelable, document.defaultView, options.button, options.pointerX, options.pointerY, options.pointerX, options.pointerY, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, element); 679 | } 680 | element.dispatchEvent(oEvent); 681 | } else { 682 | options.clientX = options.pointerX; 683 | options.clientY = options.pointerY; 684 | var evt = document.createEventObject(); 685 | oEvent = extend(evt, options); 686 | element.fireEvent('on' + eventName, oEvent); 687 | } 688 | return element; 689 | } 690 | 691 | function extend(destination, source) { 692 | for (var property in source) destination[property] = source[property]; 693 | return destination; 694 | } 695 | 696 | var eventMatchers = { 697 | 'HTMLEvents': /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/, 698 | 'MouseEvents': /^(?:click|dblclick|mouse(?:down|up|over|move|out))$/ 699 | } 700 | var defaultOptions = { 701 | pointerX: 0, 702 | pointerY: 0, 703 | button: 0, 704 | ctrlKey: false, 705 | altKey: false, 706 | shiftKey: false, 707 | metaKey: false, 708 | bubbles: true, 709 | cancelable: true 710 | } 711 | 712 | function triggerMouseEvent (node, eventType) { 713 | var clickEvent = document.createEvent ('MouseEvents'); 714 | clickEvent.initEvent (eventType, true, true); 715 | node.dispatchEvent (clickEvent); 716 | }; 717 | 718 | async function mouseDragStart(node) { 719 | console.log("Starting drag..."); 720 | triggerMouseEvent(node, "mousedown") 721 | } 722 | 723 | 724 | var progress = null; 725 | // 200 -1000 726 | async function mouseDragEnd(node,x,y){ 727 | console.log("Ending drag..."); 728 | await sleep(500) 729 | simulate(node, "mousemove",{pointerX: x-30, pointerY: y}) 730 | await sleep(500) 731 | simulate(node, "mouseup" , {pointerX: x-30, pointerY: y}) 732 | 733 | } 734 | function sleep(ms) { 735 | return new Promise(resolve => setTimeout(resolve, ms)); 736 | } 737 | 738 | async function playback(offset){ 739 | document.querySelector('#play_or_plause_player').click(); 740 | await sleep(500); 741 | const targetElement = document.querySelector("#timeshift_pointer_player") 742 | const xy = document.querySelector("#timeshift_pointer_player").getClientRects()[0] 743 | console.log(xy) 744 | mouseDragStart(targetElement); 745 | mouseDragEnd(targetElement,xy.x+offset,xy.y); 746 | }; 747 | """ 748 | , null); 749 | if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP || event.getKeyCode() == KeyEvent.KEYCODE_DPAD_DOWN || event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT || event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT || event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER || event.getKeyCode() == KeyEvent.KEYCODE_MENU) { 750 | if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP) { 751 | // 执行上一个直播地址的操作 752 | navigateToPreviousLive(); 753 | return true; // 返回 true 表示事件已处理,不传递给 WebView 754 | } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_DOWN) { 755 | // 执行下一个直播地址的操作 756 | navigateToNextLive(); 757 | return true; // 返回 true 表示事件已处理,不传递给 WebView 758 | } else if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER) { 759 | // 执行暂停操作 760 | simulateTouch(webView, 0.5f, 0.5f); 761 | return true; // 返回 true 表示事件已处理,不传递给 WebView 762 | } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) { 763 | cctvFinishedView.evaluateJavascript( 764 | """ 765 | {playback(-30);} 766 | """ 767 | , null); 768 | return true; 769 | } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) { 770 | cctvFinishedView.evaluateJavascript( 771 | """ 772 | {playback(60);} 773 | """, null); 774 | return true; 775 | } else if (event.getKeyCode() == KeyEvent.KEYCODE_MENU) { 776 | recyclerView.setVisibility(View.VISIBLE); 777 | recyclerView.requestFocus(); 778 | return false; // 返回 true 表示事件已处理,不传递给 WebView 779 | } 780 | return true; // 返回 true 表示事件已处理,不传递给 WebView 781 | } else if (event.getKeyCode() >= KeyEvent.KEYCODE_0 && event.getKeyCode() <= KeyEvent.KEYCODE_9) { 782 | int numericKey = event.getKeyCode() - KeyEvent.KEYCODE_0; 783 | 784 | // 将按下的数字键追加到缓冲区 785 | digitBuffer.append(numericKey); 786 | 787 | // 使用 Handler 来在超时后处理输入的数字 788 | new Handler().postDelayed(this::handleNumericInput, DIGIT_TIMEOUT); 789 | 790 | // 更新显示正在输入的数字的 TextView 791 | updateInputTextView(); 792 | 793 | return true; // 事件已处理,不传递给 WebView 794 | } 795 | } 796 | return super.dispatchKeyEvent(event); // 如果不处理,调用父类的方法继续传递事件 797 | } 798 | 799 | private void handleNumericInput() { 800 | // 将缓冲区中的数字转换为整数 801 | if (digitBuffer.length() > 0) { 802 | int numericValue = Integer.parseInt(digitBuffer.toString()); 803 | 804 | // 检查数字是否在有效范围内 805 | if (numericValue > 0 && numericValue <= liveUrls.length) { 806 | currentLiveIndex = numericValue - 1; 807 | loadLiveUrl(); 808 | saveCurrentLiveIndex(); // 保存当前位置 809 | } 810 | 811 | // 重置缓冲区 812 | digitBuffer.setLength(0); 813 | 814 | // 取消显示正在输入的数字 815 | inputTextView.setVisibility(View.INVISIBLE); 816 | } 817 | } 818 | 819 | @SuppressLint("SetTextI18n") 820 | private void updateInputTextView() { 821 | // 在 TextView 中显示当前正在输入的数字 822 | inputTextView.setVisibility(View.VISIBLE); 823 | inputTextView.setText("换台:" + digitBuffer.toString()); 824 | } 825 | 826 | private void loadLastLiveIndex() { 827 | SharedPreferences preferences = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); 828 | currentLiveIndex = preferences.getInt(PREF_KEY_LIVE_INDEX, 0); // 默认值为0 829 | loadLiveUrl(); // 加载上次保存的位置的直播地址 830 | } 831 | 832 | private void saveCurrentLiveIndex() { 833 | SharedPreferences preferences = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); 834 | SharedPreferences.Editor editor = preferences.edit(); 835 | editor.putInt(PREF_KEY_LIVE_INDEX, currentLiveIndex); 836 | editor.apply(); 837 | } 838 | 839 | 840 | private void loadLiveUrl() { 841 | if (currentLiveIndex>=getCCTVHeadOffset()&¤tLiveIndex<=getCCTVTailOffset()){ 842 | webView.setInitialScale(getMinimumScale()); 843 | webView.loadUrl(liveUrls[currentLiveIndex]); 844 | } 845 | 846 | if (currentLiveIndex>=getYSPHeadOffset()&¤tLiveIndex<=getYSPTailOffset()){ 847 | 848 | // 创建CookieManager对象 849 | CookieManager cookieManager = CookieManager.getInstance(); 850 | cookieManager.setAcceptCookie(true); 851 | cookieManager.setAcceptThirdPartyCookies(webView, true); 852 | Executor myExecutor = Executors.newSingleThreadExecutor(); 853 | // cookieManager.removeExpiredCookie(); 854 | // cookieManager.removeAllCookie(); 855 | // cookieManager.removeSessionCookie(); 856 | myExecutor.execute(() -> { 857 | 858 | cookieManager.setCookie(".yangshipin.cn", "yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I;Domain=.yangshipin.cn;Path=/"); 859 | cookieManager.setCookie(".yangshipin.cn", "vusession="+getYSPToken()+";Domain=.yangshipin.cn;Path=/"); 860 | }); 861 | 862 | // 将Cookie同步到WebView 863 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 864 | cookieManager.flush(); 865 | } else { 866 | CookieSyncManager.getInstance().sync(); 867 | } 868 | webView.setInitialScale(getMinimumScale()); 869 | webView.loadUrl(liveUrls[currentLiveIndex]); 870 | new Handler().postDelayed(() -> { 871 | if(webView != null) { 872 | webView.setInitialScale(getMinimumScale()); 873 | webView.reload(); 874 | } 875 | }, 1000); 876 | } 877 | } 878 | 879 | 880 | 881 | private int getCCTVHeadOffset(){ 882 | for (int i = 0; i < liveUrls.length; i++) { 883 | if (liveUrls[i].contains("cctv.com")){ 884 | return i; 885 | } 886 | } 887 | return 0; 888 | } 889 | 890 | private int getCCTVTailOffset(){ 891 | for (int i = liveUrls.length - 1; i >= 0; i--) { 892 | if (liveUrls[i].contains("cctv.com")){ 893 | return i; 894 | } 895 | } 896 | return liveUrls.length-1; 897 | } 898 | 899 | private int getYSPHeadOffset(){ 900 | for (int i = 0; i < liveUrls.length; i++) { 901 | if (liveUrls[i].contains("yangshipin.cn")){ 902 | return i; 903 | } 904 | } 905 | return 0; 906 | } 907 | 908 | private int getYSPTailOffset(){ 909 | for (int i = liveUrls.length - 1; i >= 0; i--) { 910 | if (liveUrls[i].contains("yangshipin.cn")){ 911 | return i; 912 | } 913 | } 914 | return liveUrls.length-1; 915 | } 916 | 917 | 918 | 919 | 920 | 921 | private void navigateToPreviousLive() { 922 | currentLiveIndex = (currentLiveIndex - 1 + liveUrls.length) % liveUrls.length; 923 | loadLiveUrl(); 924 | saveCurrentLiveIndex(); 925 | } 926 | 927 | private void navigateToNextLive() { 928 | currentLiveIndex = (currentLiveIndex + 1) % liveUrls.length; 929 | loadLiveUrl(); 930 | saveCurrentLiveIndex(); 931 | } 932 | 933 | 934 | private int getMinimumScale() { 935 | DisplayMetrics displayMetrics = new DisplayMetrics(); 936 | getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); 937 | int screenWidth = displayMetrics.widthPixels; 938 | int screenHeight = displayMetrics.heightPixels; 939 | 940 | // 计算缩放比例,使用 double 类型进行计算 941 | double scale = Math.min((double) screenWidth / 3840.0, (double) screenHeight / 2160.0) * 100; 942 | 943 | // 四舍五入并转为整数 944 | return (int) Math.round(scale); 945 | } 946 | 947 | // 在需要模拟触摸的地方调用该方法 948 | public void simulateTouch(View view, float x, float y) { 949 | long downTime = SystemClock.uptimeMillis(); 950 | long eventTime = SystemClock.uptimeMillis() + 100; 951 | 952 | // 构造 ACTION_DOWN 事件 953 | MotionEvent downEvent = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0); 954 | view.dispatchTouchEvent(downEvent); 955 | 956 | // 构造 ACTION_UP 事件 957 | MotionEvent upEvent = MotionEvent.obtain(downTime, eventTime + 100, MotionEvent.ACTION_UP, x, y, 0); 958 | view.dispatchTouchEvent(upEvent); 959 | 960 | // 释放事件对象 961 | downEvent.recycle(); 962 | upEvent.recycle(); 963 | } 964 | 965 | @Override 966 | public void onBackPressed() { 967 | if (recyclerView.getVisibility() == View.VISIBLE) { 968 | recyclerView.setVisibility(View.GONE); 969 | } else { 970 | if (doubleBackToExitPressedOnce) { 971 | super.onBackPressed(); 972 | return; 973 | } 974 | this.doubleBackToExitPressedOnce = true; 975 | Toast.makeText(this, "再按一次返回键退出应用", Toast.LENGTH_SHORT).show(); 976 | new Handler().postDelayed(() -> doubleBackToExitPressedOnce = false, 2000); 977 | } 978 | 979 | 980 | // 如果两秒内再次按返回键,则退出应用 981 | } 982 | 983 | private void showOverlay(String channelInfo) { 984 | // 设置覆盖层内容 985 | overlayTextView.setText(channelInfo); 986 | 987 | findViewById(R.id.overlayTextView).setVisibility(View.VISIBLE); 988 | 989 | // 使用 Handler 延时隐藏覆盖层 990 | new Handler().postDelayed(() -> { 991 | findViewById(R.id.overlayTextView).setVisibility(View.GONE); 992 | }, 3000); 993 | } 994 | 995 | @Override 996 | protected void onDestroy() { 997 | // 在销毁活动时,释放 WebView 资源 998 | if (webView != null) { 999 | webView.destroy(); 1000 | } 1001 | super.onDestroy(); 1002 | } 1003 | 1004 | 1005 | } 1006 | 1007 | 1008 | -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/MainFragment.java: -------------------------------------------------------------------------------- 1 | package com.eanyatonic.cctvViewer; 2 | 3 | import android.content.Intent; 4 | import android.graphics.Color; 5 | import android.graphics.drawable.Drawable; 6 | import android.os.Bundle; 7 | import android.os.Handler; 8 | import android.os.Looper; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.annotation.Nullable; 12 | import androidx.leanback.app.BackgroundManager; 13 | import androidx.leanback.app.BrowseSupportFragment; 14 | import androidx.leanback.widget.ArrayObjectAdapter; 15 | import androidx.leanback.widget.HeaderItem; 16 | import androidx.leanback.widget.ImageCardView; 17 | import androidx.leanback.widget.ListRow; 18 | import androidx.leanback.widget.ListRowPresenter; 19 | import androidx.leanback.widget.OnItemViewClickedListener; 20 | import androidx.leanback.widget.OnItemViewSelectedListener; 21 | import androidx.leanback.widget.Presenter; 22 | import androidx.leanback.widget.Row; 23 | import androidx.leanback.widget.RowPresenter; 24 | import androidx.core.app.ActivityOptionsCompat; 25 | import androidx.core.content.ContextCompat; 26 | 27 | import android.util.DisplayMetrics; 28 | import android.util.Log; 29 | import android.view.Gravity; 30 | import android.view.View; 31 | import android.view.ViewGroup; 32 | import android.widget.TextView; 33 | import android.widget.Toast; 34 | 35 | import com.bumptech.glide.Glide; 36 | import com.bumptech.glide.request.target.SimpleTarget; 37 | import com.bumptech.glide.request.transition.Transition; 38 | 39 | import java.util.Collections; 40 | import java.util.List; 41 | import java.util.Timer; 42 | import java.util.TimerTask; 43 | 44 | public class MainFragment extends BrowseSupportFragment { 45 | private static final String TAG = "MainFragment"; 46 | 47 | private static final int BACKGROUND_UPDATE_DELAY = 300; 48 | private static final int GRID_ITEM_WIDTH = 200; 49 | private static final int GRID_ITEM_HEIGHT = 200; 50 | private static final int NUM_ROWS = 6; 51 | private static final int NUM_COLS = 15; 52 | 53 | private final Handler mHandler = new Handler(Looper.myLooper()); 54 | private Drawable mDefaultBackground; 55 | private DisplayMetrics mMetrics; 56 | private Timer mBackgroundTimer; 57 | private String mBackgroundUri; 58 | private BackgroundManager mBackgroundManager; 59 | 60 | @Override 61 | public void onActivityCreated(Bundle savedInstanceState) { 62 | Log.i(TAG, "onCreate"); 63 | super.onActivityCreated(savedInstanceState); 64 | 65 | prepareBackgroundManager(); 66 | 67 | setupUIElements(); 68 | 69 | loadRows(); 70 | 71 | setupEventListeners(); 72 | } 73 | 74 | @Override 75 | public void onDestroy() { 76 | super.onDestroy(); 77 | if (null != mBackgroundTimer) { 78 | Log.d(TAG, "onDestroy: " + mBackgroundTimer.toString()); 79 | mBackgroundTimer.cancel(); 80 | } 81 | } 82 | 83 | private void loadRows() { 84 | List list = MovieList.setupMovies(); 85 | 86 | ArrayObjectAdapter rowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); 87 | CardPresenter cardPresenter = new CardPresenter(); 88 | 89 | int i; 90 | for (i = 0; i < NUM_ROWS; i++) { 91 | if (i != 0) { 92 | Collections.shuffle(list); 93 | } 94 | ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(cardPresenter); 95 | for (int j = 0; j < NUM_COLS; j++) { 96 | listRowAdapter.add(list.get(j % 5)); 97 | } 98 | HeaderItem header = new HeaderItem(i, MovieList.MOVIE_CATEGORY[i]); 99 | rowsAdapter.add(new ListRow(header, listRowAdapter)); 100 | } 101 | 102 | HeaderItem gridHeader = new HeaderItem(i, "PREFERENCES"); 103 | 104 | GridItemPresenter mGridPresenter = new GridItemPresenter(); 105 | ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(mGridPresenter); 106 | gridRowAdapter.add(getResources().getString(R.string.grid_view)); 107 | gridRowAdapter.add(getString(R.string.error_fragment)); 108 | gridRowAdapter.add(getResources().getString(R.string.personal_settings)); 109 | rowsAdapter.add(new ListRow(gridHeader, gridRowAdapter)); 110 | 111 | setAdapter(rowsAdapter); 112 | } 113 | 114 | private void prepareBackgroundManager() { 115 | 116 | mBackgroundManager = BackgroundManager.getInstance(getActivity()); 117 | mBackgroundManager.attach(getActivity().getWindow()); 118 | 119 | mDefaultBackground = ContextCompat.getDrawable(getActivity(), R.drawable.default_background); 120 | mMetrics = new DisplayMetrics(); 121 | getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics); 122 | } 123 | 124 | private void setupUIElements() { 125 | // setBadgeDrawable(getActivity().getResources().getDrawable( 126 | // R.drawable.videos_by_google_banner)); 127 | setTitle(getString(R.string.browse_title)); // Badge, when set, takes precedent 128 | // over title 129 | setHeadersState(HEADERS_ENABLED); 130 | setHeadersTransitionOnBackEnabled(true); 131 | 132 | // set fastLane (or headers) background color 133 | setBrandColor(ContextCompat.getColor(getActivity(), R.color.fastlane_background)); 134 | // set search icon color 135 | setSearchAffordanceColor(ContextCompat.getColor(getActivity(), R.color.search_opaque)); 136 | } 137 | 138 | private void setupEventListeners() { 139 | setOnSearchClickedListener(view -> 140 | Toast.makeText(getActivity(), "Implement your own in-app search", Toast.LENGTH_LONG).show()); 141 | 142 | setOnItemViewClickedListener(new ItemViewClickedListener()); 143 | setOnItemViewSelectedListener(new ItemViewSelectedListener()); 144 | } 145 | 146 | private void updateBackground(String uri) { 147 | int width = mMetrics.widthPixels; 148 | int height = mMetrics.heightPixels; 149 | Glide.with(getActivity()) 150 | .load(uri) 151 | .centerCrop() 152 | .error(mDefaultBackground) 153 | .into(new SimpleTarget(width, height) { 154 | @Override 155 | public void onResourceReady(@NonNull Drawable drawable, 156 | @Nullable Transition transition) { 157 | mBackgroundManager.setDrawable(drawable); 158 | } 159 | }); 160 | mBackgroundTimer.cancel(); 161 | } 162 | 163 | private void startBackgroundTimer() { 164 | if (null != mBackgroundTimer) { 165 | mBackgroundTimer.cancel(); 166 | } 167 | mBackgroundTimer = new Timer(); 168 | mBackgroundTimer.schedule(new UpdateBackgroundTask(), BACKGROUND_UPDATE_DELAY); 169 | } 170 | 171 | private final class ItemViewClickedListener implements OnItemViewClickedListener { 172 | @Override 173 | public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, 174 | RowPresenter.ViewHolder rowViewHolder, Row row) { 175 | 176 | if (item instanceof Movie) { 177 | Movie movie = (Movie) item; 178 | Log.d(TAG, "Item: " + item.toString()); 179 | Intent intent = new Intent(getActivity(), DetailsActivity.class); 180 | intent.putExtra(DetailsActivity.MOVIE, movie); 181 | 182 | Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( 183 | getActivity(), 184 | ((ImageCardView) itemViewHolder.view).getMainImageView(), 185 | DetailsActivity.SHARED_ELEMENT_NAME) 186 | .toBundle(); 187 | getActivity().startActivity(intent, bundle); 188 | } else if (item instanceof String) { 189 | if (((String) item).contains(getString(R.string.error_fragment))) { 190 | Intent intent = new Intent(getActivity(), BrowseErrorActivity.class); 191 | startActivity(intent); 192 | } else { 193 | Toast.makeText(getActivity(), ((String) item), Toast.LENGTH_SHORT).show(); 194 | } 195 | } 196 | } 197 | } 198 | 199 | private final class ItemViewSelectedListener implements OnItemViewSelectedListener { 200 | @Override 201 | public void onItemSelected( 202 | Presenter.ViewHolder itemViewHolder, 203 | Object item, 204 | RowPresenter.ViewHolder rowViewHolder, 205 | Row row) { 206 | if (item instanceof Movie) { 207 | mBackgroundUri = ((Movie) item).getBackgroundImageUrl(); 208 | startBackgroundTimer(); 209 | } 210 | } 211 | } 212 | 213 | private class UpdateBackgroundTask extends TimerTask { 214 | 215 | @Override 216 | public void run() { 217 | mHandler.post(new Runnable() { 218 | @Override 219 | public void run() { 220 | updateBackground(mBackgroundUri); 221 | } 222 | }); 223 | } 224 | } 225 | 226 | private class GridItemPresenter extends Presenter { 227 | @Override 228 | public ViewHolder onCreateViewHolder(ViewGroup parent) { 229 | TextView view = new TextView(parent.getContext()); 230 | view.setLayoutParams(new ViewGroup.LayoutParams(GRID_ITEM_WIDTH, GRID_ITEM_HEIGHT)); 231 | view.setFocusable(true); 232 | view.setFocusableInTouchMode(true); 233 | view.setBackgroundColor( 234 | ContextCompat.getColor(getActivity(), R.color.default_background)); 235 | view.setTextColor(Color.WHITE); 236 | view.setGravity(Gravity.CENTER); 237 | return new ViewHolder(view); 238 | } 239 | 240 | @Override 241 | public void onBindViewHolder(ViewHolder viewHolder, Object item) { 242 | ((TextView) viewHolder.view).setText((String) item); 243 | } 244 | 245 | @Override 246 | public void onUnbindViewHolder(ViewHolder viewHolder) { 247 | } 248 | } 249 | 250 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/Movie.java: -------------------------------------------------------------------------------- 1 | package com.eanyatonic.cctvViewer; 2 | 3 | import java.io.Serializable; 4 | 5 | /* 6 | * Movie class represents video entity with title, description, image thumbs and video url. 7 | */ 8 | public class Movie implements Serializable { 9 | static final long serialVersionUID = 727566175075960653L; 10 | private long id; 11 | private String title; 12 | private String description; 13 | private String bgImageUrl; 14 | private String cardImageUrl; 15 | private String videoUrl; 16 | private String studio; 17 | 18 | public Movie() { 19 | } 20 | 21 | public long getId() { 22 | return id; 23 | } 24 | 25 | public void setId(long id) { 26 | this.id = id; 27 | } 28 | 29 | public String getTitle() { 30 | return title; 31 | } 32 | 33 | public void setTitle(String title) { 34 | this.title = title; 35 | } 36 | 37 | public String getDescription() { 38 | return description; 39 | } 40 | 41 | public void setDescription(String description) { 42 | this.description = description; 43 | } 44 | 45 | public String getStudio() { 46 | return studio; 47 | } 48 | 49 | public void setStudio(String studio) { 50 | this.studio = studio; 51 | } 52 | 53 | public String getVideoUrl() { 54 | return videoUrl; 55 | } 56 | 57 | public void setVideoUrl(String videoUrl) { 58 | this.videoUrl = videoUrl; 59 | } 60 | 61 | public String getBackgroundImageUrl() { 62 | return bgImageUrl; 63 | } 64 | 65 | public void setBackgroundImageUrl(String bgImageUrl) { 66 | this.bgImageUrl = bgImageUrl; 67 | } 68 | 69 | public String getCardImageUrl() { 70 | return cardImageUrl; 71 | } 72 | 73 | public void setCardImageUrl(String cardImageUrl) { 74 | this.cardImageUrl = cardImageUrl; 75 | } 76 | 77 | @Override 78 | public String toString() { 79 | return "Movie{" + 80 | "id=" + id + 81 | ", title='" + title + '\'' + 82 | ", videoUrl='" + videoUrl + '\'' + 83 | ", backgroundImageUrl='" + bgImageUrl + '\'' + 84 | ", cardImageUrl='" + cardImageUrl + '\'' + 85 | '}'; 86 | } 87 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/MovieList.java: -------------------------------------------------------------------------------- 1 | package com.eanyatonic.cctvViewer; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public final class MovieList { 7 | public static final String MOVIE_CATEGORY[] = { 8 | "Category Zero", 9 | "Category One", 10 | "Category Two", 11 | "Category Three", 12 | "Category Four", 13 | "Category Five", 14 | }; 15 | 16 | private static List list; 17 | private static long count = 0; 18 | 19 | public static List getList() { 20 | if (list == null) { 21 | list = setupMovies(); 22 | } 23 | return list; 24 | } 25 | 26 | public static List setupMovies() { 27 | list = new ArrayList<>(); 28 | String title[] = { 29 | "Zeitgeist 2010_ Year in Review", 30 | "Google Demo Slam_ 20ft Search", 31 | "Introducing Gmail Blue", 32 | "Introducing Google Fiber to the Pole", 33 | "Introducing Google Nose" 34 | }; 35 | 36 | String description = "Fusce id nisi turpis. Praesent viverra bibendum semper. " 37 | + "Donec tristique, orci sed semper lacinia, quam erat rhoncus massa, non congue tellus est " 38 | + "quis tellus. Sed mollis orci venenatis quam scelerisque accumsan. Curabitur a massa sit " 39 | + "amet mi accumsan mollis sed et magna. Vivamus sed aliquam risus. Nulla eget dolor in elit " 40 | + "facilisis mattis. Ut aliquet luctus lacus. Phasellus nec commodo erat. Praesent tempus id " 41 | + "lectus ac scelerisque. Maecenas pretium cursus lectus id volutpat."; 42 | String studio[] = { 43 | "Studio Zero", "Studio One", "Studio Two", "Studio Three", "Studio Four" 44 | }; 45 | String videoUrl[] = { 46 | "https://commondatastorage.googleapis.com/android-tv/Sample%20videos/Zeitgeist/Zeitgeist%202010_%20Year%20in%20Review.mp4", 47 | "https://commondatastorage.googleapis.com/android-tv/Sample%20videos/Demo%20Slam/Google%20Demo%20Slam_%2020ft%20Search.mp4", 48 | "https://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Gmail%20Blue.mp4", 49 | "https://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Google%20Fiber%20to%20the%20Pole.mp4", 50 | "https://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Google%20Nose.mp4" 51 | }; 52 | String bgImageUrl[] = { 53 | "https://commondatastorage.googleapis.com/android-tv/Sample%20videos/Zeitgeist/Zeitgeist%202010_%20Year%20in%20Review/bg.jpg", 54 | "https://commondatastorage.googleapis.com/android-tv/Sample%20videos/Demo%20Slam/Google%20Demo%20Slam_%2020ft%20Search/bg.jpg", 55 | "https://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Gmail%20Blue/bg.jpg", 56 | "https://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Google%20Fiber%20to%20the%20Pole/bg.jpg", 57 | "https://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Google%20Nose/bg.jpg", 58 | }; 59 | String cardImageUrl[] = { 60 | "https://commondatastorage.googleapis.com/android-tv/Sample%20videos/Zeitgeist/Zeitgeist%202010_%20Year%20in%20Review/card.jpg", 61 | "https://commondatastorage.googleapis.com/android-tv/Sample%20videos/Demo%20Slam/Google%20Demo%20Slam_%2020ft%20Search/card.jpg", 62 | "https://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Gmail%20Blue/card.jpg", 63 | "https://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Google%20Fiber%20to%20the%20Pole/card.jpg", 64 | "https://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Google%20Nose/card.jpg" 65 | }; 66 | 67 | for (int index = 0; index < title.length; ++index) { 68 | list.add( 69 | buildMovieInfo( 70 | title[index], 71 | description, 72 | studio[index], 73 | videoUrl[index], 74 | cardImageUrl[index], 75 | bgImageUrl[index])); 76 | } 77 | 78 | return list; 79 | } 80 | 81 | private static Movie buildMovieInfo( 82 | String title, 83 | String description, 84 | String studio, 85 | String videoUrl, 86 | String cardImageUrl, 87 | String backgroundImageUrl) { 88 | Movie movie = new Movie(); 89 | movie.setId(count++); 90 | movie.setTitle(title); 91 | movie.setDescription(description); 92 | movie.setStudio(studio); 93 | movie.setCardImageUrl(cardImageUrl); 94 | movie.setBackgroundImageUrl(backgroundImageUrl); 95 | movie.setVideoUrl(videoUrl); 96 | return movie; 97 | } 98 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/PlaybackActivity.java: -------------------------------------------------------------------------------- 1 | package com.eanyatonic.cctvViewer; 2 | 3 | import android.os.Bundle; 4 | 5 | import androidx.fragment.app.FragmentActivity; 6 | 7 | /** 8 | * Loads {@link PlaybackVideoFragment}. 9 | */ 10 | public class PlaybackActivity extends FragmentActivity { 11 | 12 | @Override 13 | public void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | if (savedInstanceState == null) { 16 | getSupportFragmentManager() 17 | .beginTransaction() 18 | .replace(android.R.id.content, new PlaybackVideoFragment()) 19 | .commit(); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/PlaybackVideoFragment.java: -------------------------------------------------------------------------------- 1 | package com.eanyatonic.cctvViewer; 2 | 3 | import android.net.Uri; 4 | import android.os.Bundle; 5 | 6 | import androidx.leanback.app.VideoSupportFragment; 7 | import androidx.leanback.app.VideoSupportFragmentGlueHost; 8 | import androidx.leanback.media.MediaPlayerAdapter; 9 | import androidx.leanback.media.PlaybackTransportControlGlue; 10 | import androidx.leanback.widget.PlaybackControlsRow; 11 | 12 | /** 13 | * Handles video playback with media controls. 14 | */ 15 | public class PlaybackVideoFragment extends VideoSupportFragment { 16 | 17 | private PlaybackTransportControlGlue mTransportControlGlue; 18 | 19 | @Override 20 | public void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | 23 | final Movie movie = 24 | (Movie) getActivity().getIntent().getSerializableExtra(DetailsActivity.MOVIE); 25 | 26 | VideoSupportFragmentGlueHost glueHost = 27 | new VideoSupportFragmentGlueHost(PlaybackVideoFragment.this); 28 | 29 | MediaPlayerAdapter playerAdapter = new MediaPlayerAdapter(getActivity()); 30 | playerAdapter.setRepeatAction(PlaybackControlsRow.RepeatAction.INDEX_NONE); 31 | 32 | mTransportControlGlue = new PlaybackTransportControlGlue<>(getActivity(), playerAdapter); 33 | mTransportControlGlue.setHost(glueHost); 34 | mTransportControlGlue.setTitle(movie.getTitle()); 35 | mTransportControlGlue.setSubtitle(movie.getDescription()); 36 | mTransportControlGlue.playWhenPrepared(); 37 | playerAdapter.setDataSource(Uri.parse(movie.getVideoUrl())); 38 | } 39 | 40 | @Override 41 | public void onPause() { 42 | super.onPause(); 43 | if (mTransportControlGlue != null) { 44 | mTransportControlGlue.pause(); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/VideoDetailsFragment.java: -------------------------------------------------------------------------------- 1 | package com.eanyatonic.cctvViewer; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.graphics.Bitmap; 6 | import android.os.Bundle; 7 | import android.graphics.drawable.Drawable; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.Nullable; 11 | import androidx.leanback.app.DetailsSupportFragment; 12 | import androidx.leanback.app.DetailsSupportFragmentBackgroundController; 13 | import androidx.leanback.widget.Action; 14 | import androidx.leanback.widget.ArrayObjectAdapter; 15 | import androidx.leanback.widget.ClassPresenterSelector; 16 | import androidx.leanback.widget.DetailsOverviewRow; 17 | import androidx.leanback.widget.FullWidthDetailsOverviewRowPresenter; 18 | import androidx.leanback.widget.FullWidthDetailsOverviewSharedElementHelper; 19 | import androidx.leanback.widget.HeaderItem; 20 | import androidx.leanback.widget.ImageCardView; 21 | import androidx.leanback.widget.ListRow; 22 | import androidx.leanback.widget.ListRowPresenter; 23 | import androidx.leanback.widget.OnActionClickedListener; 24 | import androidx.leanback.widget.OnItemViewClickedListener; 25 | import androidx.leanback.widget.Presenter; 26 | import androidx.leanback.widget.Row; 27 | import androidx.leanback.widget.RowPresenter; 28 | import androidx.core.app.ActivityOptionsCompat; 29 | import androidx.core.content.ContextCompat; 30 | 31 | import android.util.Log; 32 | import android.widget.Toast; 33 | 34 | import com.bumptech.glide.Glide; 35 | import com.bumptech.glide.request.target.SimpleTarget; 36 | import com.bumptech.glide.request.transition.Transition; 37 | 38 | import java.util.Collections; 39 | import java.util.List; 40 | 41 | /* 42 | * LeanbackDetailsFragment extends DetailsFragment, a Wrapper fragment for leanback details screens. 43 | * It shows a detailed view of video and its meta plus related videos. 44 | */ 45 | public class VideoDetailsFragment extends DetailsSupportFragment { 46 | private static final String TAG = "VideoDetailsFragment"; 47 | 48 | private static final int ACTION_WATCH_TRAILER = 1; 49 | private static final int ACTION_RENT = 2; 50 | private static final int ACTION_BUY = 3; 51 | 52 | private static final int DETAIL_THUMB_WIDTH = 274; 53 | private static final int DETAIL_THUMB_HEIGHT = 274; 54 | 55 | private static final int NUM_COLS = 10; 56 | 57 | private Movie mSelectedMovie; 58 | 59 | private ArrayObjectAdapter mAdapter; 60 | private ClassPresenterSelector mPresenterSelector; 61 | 62 | private DetailsSupportFragmentBackgroundController mDetailsBackground; 63 | 64 | @Override 65 | public void onCreate(Bundle savedInstanceState) { 66 | Log.d(TAG, "onCreate DetailsFragment"); 67 | super.onCreate(savedInstanceState); 68 | 69 | mDetailsBackground = new DetailsSupportFragmentBackgroundController(this); 70 | 71 | mSelectedMovie = 72 | (Movie) getActivity().getIntent().getSerializableExtra(DetailsActivity.MOVIE); 73 | if (mSelectedMovie != null) { 74 | mPresenterSelector = new ClassPresenterSelector(); 75 | mAdapter = new ArrayObjectAdapter(mPresenterSelector); 76 | setupDetailsOverviewRow(); 77 | setupDetailsOverviewRowPresenter(); 78 | setupRelatedMovieListRow(); 79 | setAdapter(mAdapter); 80 | initializeBackground(mSelectedMovie); 81 | setOnItemViewClickedListener(new ItemViewClickedListener()); 82 | } else { 83 | Intent intent = new Intent(getActivity(), MainActivity.class); 84 | startActivity(intent); 85 | } 86 | } 87 | 88 | private void initializeBackground(Movie data) { 89 | mDetailsBackground.enableParallax(); 90 | Glide.with(getActivity()) 91 | .asBitmap() 92 | .centerCrop() 93 | .error(R.drawable.default_background) 94 | .load(data.getBackgroundImageUrl()) 95 | .into(new SimpleTarget() { 96 | @Override 97 | public void onResourceReady(@NonNull Bitmap bitmap, 98 | @Nullable Transition transition) { 99 | mDetailsBackground.setCoverBitmap(bitmap); 100 | mAdapter.notifyArrayItemRangeChanged(0, mAdapter.size()); 101 | } 102 | }); 103 | } 104 | 105 | private void setupDetailsOverviewRow() { 106 | Log.d(TAG, "doInBackground: " + mSelectedMovie.toString()); 107 | final DetailsOverviewRow row = new DetailsOverviewRow(mSelectedMovie); 108 | row.setImageDrawable( 109 | ContextCompat.getDrawable(getActivity(), R.drawable.default_background)); 110 | int width = convertDpToPixel(getActivity().getApplicationContext(), DETAIL_THUMB_WIDTH); 111 | int height = convertDpToPixel(getActivity().getApplicationContext(), DETAIL_THUMB_HEIGHT); 112 | Glide.with(getActivity()) 113 | .load(mSelectedMovie.getCardImageUrl()) 114 | .centerCrop() 115 | .error(R.drawable.default_background) 116 | .into(new SimpleTarget(width, height) { 117 | @Override 118 | public void onResourceReady(@NonNull Drawable drawable, 119 | @Nullable Transition transition) { 120 | Log.d(TAG, "details overview card image url ready: " + drawable); 121 | row.setImageDrawable(drawable); 122 | mAdapter.notifyArrayItemRangeChanged(0, mAdapter.size()); 123 | } 124 | }); 125 | 126 | ArrayObjectAdapter actionAdapter = new ArrayObjectAdapter(); 127 | 128 | actionAdapter.add( 129 | new Action( 130 | ACTION_WATCH_TRAILER, 131 | getResources().getString(R.string.watch_trailer_1), 132 | getResources().getString(R.string.watch_trailer_2))); 133 | actionAdapter.add( 134 | new Action( 135 | ACTION_RENT, 136 | getResources().getString(R.string.rent_1), 137 | getResources().getString(R.string.rent_2))); 138 | actionAdapter.add( 139 | new Action( 140 | ACTION_BUY, 141 | getResources().getString(R.string.buy_1), 142 | getResources().getString(R.string.buy_2))); 143 | row.setActionsAdapter(actionAdapter); 144 | 145 | mAdapter.add(row); 146 | } 147 | 148 | private void setupDetailsOverviewRowPresenter() { 149 | // Set detail background. 150 | FullWidthDetailsOverviewRowPresenter detailsPresenter = 151 | new FullWidthDetailsOverviewRowPresenter(new DetailsDescriptionPresenter()); 152 | detailsPresenter.setBackgroundColor( 153 | ContextCompat.getColor(getActivity(), R.color.selected_background)); 154 | 155 | // Hook up transition element. 156 | FullWidthDetailsOverviewSharedElementHelper sharedElementHelper = 157 | new FullWidthDetailsOverviewSharedElementHelper(); 158 | sharedElementHelper.setSharedElementEnterTransition( 159 | getActivity(), DetailsActivity.SHARED_ELEMENT_NAME); 160 | detailsPresenter.setListener(sharedElementHelper); 161 | detailsPresenter.setParticipatingEntranceTransition(true); 162 | 163 | detailsPresenter.setOnActionClickedListener(new OnActionClickedListener() { 164 | @Override 165 | public void onActionClicked(Action action) { 166 | if (action.getId() == ACTION_WATCH_TRAILER) { 167 | Intent intent = new Intent(getActivity(), PlaybackActivity.class); 168 | intent.putExtra(DetailsActivity.MOVIE, mSelectedMovie); 169 | startActivity(intent); 170 | } else { 171 | Toast.makeText(getActivity(), action.toString(), Toast.LENGTH_SHORT).show(); 172 | } 173 | } 174 | }); 175 | mPresenterSelector.addClassPresenter(DetailsOverviewRow.class, detailsPresenter); 176 | } 177 | 178 | private void setupRelatedMovieListRow() { 179 | String subcategories[] = {getString(R.string.related_movies)}; 180 | List list = MovieList.getList(); 181 | 182 | Collections.shuffle(list); 183 | ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter()); 184 | for (int j = 0; j < NUM_COLS; j++) { 185 | listRowAdapter.add(list.get(j % 5)); 186 | } 187 | 188 | HeaderItem header = new HeaderItem(0, subcategories[0]); 189 | mAdapter.add(new ListRow(header, listRowAdapter)); 190 | mPresenterSelector.addClassPresenter(ListRow.class, new ListRowPresenter()); 191 | } 192 | 193 | private int convertDpToPixel(Context context, int dp) { 194 | float density = context.getResources().getDisplayMetrics().density; 195 | return Math.round((float) dp * density); 196 | } 197 | 198 | private final class ItemViewClickedListener implements OnItemViewClickedListener { 199 | @Override 200 | public void onItemClicked( 201 | Presenter.ViewHolder itemViewHolder, 202 | Object item, 203 | RowPresenter.ViewHolder rowViewHolder, 204 | Row row) { 205 | 206 | if (item instanceof Movie) { 207 | Log.d(TAG, "Item: " + item.toString()); 208 | Intent intent = new Intent(getActivity(), DetailsActivity.class); 209 | intent.putExtra(getResources().getString(R.string.movie), mSelectedMovie); 210 | 211 | Bundle bundle = 212 | ActivityOptionsCompat.makeSceneTransitionAnimation( 213 | getActivity(), 214 | ((ImageCardView) itemViewHolder.view).getMainImageView(), 215 | DetailsActivity.SHARED_ELEMENT_NAME) 216 | .toBundle(); 217 | getActivity().startActivity(intent, bundle); 218 | } 219 | } 220 | } 221 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/bean/EpgInfo.java: -------------------------------------------------------------------------------- 1 | package com.eanyatonic.cctvViewer.bean; 2 | 3 | public class EpgInfo { 4 | private String id; 5 | private String name; 6 | 7 | public EpgInfo(String id, String name) { 8 | this.id = id; 9 | this.name = name; 10 | } 11 | 12 | public String getId() { 13 | return id; 14 | } 15 | 16 | public void setId(String id) { 17 | this.id = id; 18 | } 19 | 20 | public String getName() { 21 | return name; 22 | } 23 | 24 | public void setName(String name) { 25 | this.name = name; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/tools/FileTool.java: -------------------------------------------------------------------------------- 1 | 2 | package com.eanyatonic.cctvViewer.tools; 3 | 4 | import android.content.Context; 5 | import android.content.res.AssetManager; 6 | import android.util.Log; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.InputStreamReader; 12 | import java.io.Reader; 13 | import java.nio.charset.StandardCharsets; 14 | 15 | public class FileTool { 16 | 17 | private Context context; 18 | 19 | public FileTool(Context context) { 20 | this.context = context; 21 | } 22 | 23 | public String readFileContent(String fileName) { 24 | InputStream inputStream = null; 25 | Reader reader = null; 26 | BufferedReader bufferedReader = null; 27 | StringBuilder result = new StringBuilder(); 28 | 29 | try { 30 | //得到资源中的asset数据流 31 | inputStream = context.getResources().getAssets().open(fileName); 32 | reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);// 字符流 33 | bufferedReader = new BufferedReader(reader); //缓冲流 34 | String temp; 35 | while ((temp = bufferedReader.readLine()) != null) { 36 | result.append(temp); 37 | } 38 | Log.i("fileTool", "result:" + result); 39 | } catch (Exception e) { 40 | Log.e("fileError", e.toString()); 41 | } finally { 42 | if (reader != null) { 43 | try { 44 | reader.close(); 45 | } catch (IOException e) { 46 | e.printStackTrace(); 47 | } 48 | } 49 | if (inputStream != null) { 50 | try { 51 | inputStream.close(); 52 | } catch (IOException e) { 53 | e.printStackTrace(); 54 | } 55 | } 56 | if (bufferedReader != null) { 57 | try { 58 | bufferedReader.close(); 59 | } catch (IOException e) { 60 | e.printStackTrace(); 61 | } 62 | } 63 | 64 | } 65 | 66 | return result.toString(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/tools/FileUtils.java: -------------------------------------------------------------------------------- 1 | package com.eanyatonic.cctvViewer.tools; 2 | 3 | import android.content.Context; 4 | 5 | import java.io.File; 6 | import java.io.FileOutputStream; 7 | import java.io.InputStream; 8 | 9 | /** 10 | * 文件操作工具类 11 | *

12 | * 将存储文件目录规范化,对应功能划分目录,后续操作缓存管理进行调用操作 13 | */ 14 | public class FileUtils { 15 | 16 | /** 17 | * 保存文件预览的目录 18 | * 19 | * @param context 上下文对象 20 | */ 21 | public static File getTBSFileDir(Context context) { 22 | String dirName = "TBSFile"; 23 | return context.getExternalFilesDir(dirName); 24 | } 25 | 26 | /** 27 | * 把asset的文件转化为本地文件 28 | * 29 | * @param context 上下文对象 30 | * @param oldPath 旧的文件路径 31 | * @param newPath 新的文件路径 32 | */ 33 | public static boolean copyAssets(Context context, String oldPath, String newPath) { 34 | try { 35 | String fileNames[] = context.getAssets().list(oldPath);// 获取assets目录下的所有文件及目录名 36 | if (fileNames.length > 0) {// 如果是目录 37 | File file = new File(newPath); 38 | file.mkdirs();// 如果文件夹不存在,则递归 39 | for (String fileName : fileNames) { 40 | copyAssets(context, oldPath + "/" + fileName, newPath + "/" + fileName); 41 | } 42 | } else {// 如果是文件 43 | InputStream is = context.getAssets().open(oldPath); 44 | FileOutputStream fos = new FileOutputStream(new File(newPath)); 45 | byte[] buffer = new byte[1024]; 46 | int byteCount; 47 | while ((byteCount = is.read(buffer)) != -1) {// 循环从输入流读取 48 | // buffer字节 49 | fos.write(buffer, 0, byteCount);// 将读取的输入流写入到输出流 50 | } 51 | fos.flush();// 刷新缓冲区 52 | is.close(); 53 | fos.close(); 54 | } 55 | } catch (Exception e) { 56 | e.printStackTrace(); 57 | return false; 58 | } 59 | return true; 60 | } 61 | 62 | /** 63 | * 获取url文件后缀 64 | * 65 | * @param url 文件链接 66 | */ 67 | public static String getSuffix(String url) { 68 | if ((url != null) && (url.length() > 0)) { 69 | int dot = url.lastIndexOf('.'); 70 | if ((dot > -1) && (dot < (url.length() - 1))) { 71 | return url.substring(dot + 1); 72 | } 73 | } 74 | return ""; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/tools/RestartAppUtil.java: -------------------------------------------------------------------------------- 1 | package com.eanyatonic.cctvViewer.tools; 2 | 3 | import static android.content.Context.JOB_SCHEDULER_SERVICE; 4 | import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 5 | 6 | import android.annotation.TargetApi; 7 | import android.app.ActivityManager; 8 | import android.app.AlarmManager; 9 | import android.app.PendingIntent; 10 | import android.app.job.JobInfo; 11 | import android.app.job.JobParameters; 12 | import android.app.job.JobScheduler; 13 | import android.app.job.JobService; 14 | import android.content.ComponentName; 15 | import android.content.Context; 16 | import android.content.Intent; 17 | import android.os.Build; 18 | 19 | /** 20 | * 多种方式重启应用自身 21 | */ 22 | 23 | public class RestartAppUtil 24 | { 25 | 26 | /** 27 | * 使用 AlarmManager 来帮助重启 28 | * 29 | * @param context 30 | * @param cls 31 | */ 32 | public static void restartByAlarm(Context context, Class cls) 33 | { 34 | Intent mStartActivity = new Intent(context, cls); 35 | int mPendingIntentId = 123456; 36 | PendingIntent pIntent = PendingIntent.getActivity(context, mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); 37 | 38 | AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 39 | alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + 500, pIntent); 40 | 41 | System.exit(0); 42 | } 43 | 44 | /** 45 | * 使用 killProcess 杀死自身,系统会恢复应用 46 | * 47 | * @param context 48 | * @param cls 49 | */ 50 | public static void restartByKillProcess(Context context, Class cls) 51 | { 52 | Intent intent = new Intent(context, cls); 53 | intent.addFlags(FLAG_ACTIVITY_NEW_TASK); 54 | context.startActivity(intent); 55 | android.os.Process.killProcess(android.os.Process.myPid()); 56 | } 57 | 58 | /** 59 | * 通过清栈触发应用重启。但不会重启 application ,与应用相关的静态变量也会更重启前一样。 60 | * 61 | * @param context 62 | */ 63 | public static void restartByClearTop(Context context) 64 | { 65 | Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()); 66 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 67 | context.startActivity(intent); 68 | } 69 | 70 | /** 71 | * 利用系统重启api触发应用重启 72 | * 73 | * @param context 74 | */ 75 | public static void restartBySystemApi(Context context) 76 | { 77 | ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 78 | manager.restartPackage(context.getPackageName()); 79 | } 80 | 81 | /** 82 | * 通过 Intent.makeRestartActivityTask 来触发应用重启,跟 restartByClearTop 类似。 83 | * 但不会重启 application ,与应用相关的静态变量也会更重启前一样。 84 | * 85 | * @param context 86 | */ 87 | public static void restartByCompatApi(Context context, Class cls) 88 | { 89 | Intent intent = new Intent(context, cls); 90 | Intent restartIntent = Intent.makeRestartActivityTask(intent.getComponent()); 91 | context.startActivity(restartIntent); 92 | System.exit(0); 93 | } 94 | 95 | /** 96 | * 5.1 版本以后可以借助 JobScheduler 来重启应用 97 | * 98 | * @param context 99 | */ 100 | public static void restartByJobScheduler(Context context, Class cls) 101 | { 102 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) 103 | { 104 | int delayTimeMin = 1000; 105 | int delayTimeMax = 2000; 106 | 107 | MyJobSchedulerService.setMainIntent(new Intent(context, cls)); 108 | 109 | JobInfo.Builder jobInfoBuild = new JobInfo.Builder(0, new ComponentName(context, MyJobSchedulerService.class)); 110 | jobInfoBuild.setMinimumLatency(delayTimeMin); 111 | jobInfoBuild.setOverrideDeadline(delayTimeMax); 112 | JobScheduler jobScheduler = (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE); 113 | jobScheduler.schedule(jobInfoBuild.build()); 114 | 115 | System.exit(0); 116 | } 117 | } 118 | 119 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 120 | static class MyJobSchedulerService extends JobService 121 | { 122 | private static Intent mIntent; 123 | 124 | public static void setMainIntent(Intent intent) 125 | { 126 | mIntent = intent; 127 | } 128 | 129 | @Override 130 | public boolean onStartJob(JobParameters params) 131 | { 132 | startActivity(mIntent); 133 | jobFinished(params, false); 134 | return false; 135 | } 136 | 137 | @Override 138 | public boolean onStopJob(JobParameters params) 139 | { 140 | return false; 141 | } 142 | } 143 | 144 | } 145 | 146 | -------------------------------------------------------------------------------- /app/src/main/java/com/eanyatonic/cctvViewer/tools/SysTool.java: -------------------------------------------------------------------------------- 1 | package com.eanyatonic.cctvViewer.tools; 2 | 3 | import android.os.Build; 4 | import android.util.Log; 5 | 6 | public class SysTool { 7 | public static String showSysAach(){ 8 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 9 | return Build.SUPPORTED_ABIS[0]; 10 | } else { 11 | return Build.CPU_ABI; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/app_icon_your_company.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zcodeoooo/CCTV_Viewer/2e99f56235e9beda50d7a6689b01ed610fe055b4/app/src/main/res/drawable/app_icon_your_company.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/channel_background_selected.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/channel_background_unselected.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/default_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zcodeoooo/CCTV_Viewer/2e99f56235e9beda50d7a6689b01ed610fe055b4/app/src/main/res/drawable/logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/movie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zcodeoooo/CCTV_Viewer/2e99f56235e9beda50d7a6689b01ed610fe055b4/app/src/main/res/drawable/movie.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_background.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_details.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 14 | 21 | 22 | 23 | 27 | 28 | 29 | 40 | 41 | 54 | 55 | 61 | 62 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_channel.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zcodeoooo/CCTV_Viewer/2e99f56235e9beda50d7a6689b01ed610fe055b4/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zcodeoooo/CCTV_Viewer/2e99f56235e9beda50d7a6689b01ed610fe055b4/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zcodeoooo/CCTV_Viewer/2e99f56235e9beda50d7a6689b01ed610fe055b4/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zcodeoooo/CCTV_Viewer/2e99f56235e9beda50d7a6689b01ed610fe055b4/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zcodeoooo/CCTV_Viewer/2e99f56235e9beda50d7a6689b01ed610fe055b4/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #000000 3 | #DDDDDD 4 | #0096a6 5 | #ffaa3f 6 | #3d3d3d 7 | #ffaa3f 8 | 9 | 10 | 11 | #80FFAA3F 12 | #80D3A229 13 | #80FF0078 14 | #FF0078 15 | #00000000 16 | #00FFE1 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 胖弟看电视 3 | Videos by Your Company 4 | Related Videos 5 | Grid View 6 | Error Fragment 7 | Personal Settings 8 | Watch trailer 9 | FREE 10 | Rent By Day 11 | From $1.99 12 | Buy and Own 13 | AT $9.99 14 | Movie 15 | 16 | 17 | An error occurred 18 | Dismiss 19 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |