├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── libs │ └── bugly_crashreport_upgrade-1.4.2.aar ├── proguard-rules.pro ├── src │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── z │ │ │ └── exoplayertest │ │ │ └── ExampleInstrumentedTest.java │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── assets │ │ │ └── channel │ │ ├── java │ │ │ └── com │ │ │ │ └── z │ │ │ │ └── exoplayertest │ │ │ │ ├── adpter │ │ │ │ ├── ChannelAdaptr.java │ │ │ │ ├── LikeAdaptr.java │ │ │ │ └── PagerAdapter.java │ │ │ │ ├── database │ │ │ │ ├── Channel.java │ │ │ │ ├── ChannelDao.java │ │ │ │ └── SqliteHelper.java │ │ │ │ ├── utils │ │ │ │ ├── AppUtils.java │ │ │ │ ├── Density.java │ │ │ │ └── FileUtil.java │ │ │ │ └── view │ │ │ │ ├── BaseActivity.java │ │ │ │ ├── BaseApplication.java │ │ │ │ ├── BaseFragment.java │ │ │ │ ├── ChannelFragment.java │ │ │ │ ├── FetchPatchHandler.java │ │ │ │ ├── LikeFragment.java │ │ │ │ ├── MainActivity.java │ │ │ │ ├── SelectTvActivity.java │ │ │ │ ├── SettingFragment.java │ │ │ │ └── WaveView.java │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ ├── edit_bg.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── player_bottom_bg.xml │ │ │ ├── ripple.xml │ │ │ └── search_bg.xml │ │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ ├── activity_select_tv.xml │ │ │ ├── channel_item.xml │ │ │ ├── content_main.xml │ │ │ ├── dialog_edit.xml │ │ │ ├── dialog_feedback.xml │ │ │ ├── fragment_channel.xml │ │ │ ├── fragment_setting.xml │ │ │ ├── playback_control_view.xml │ │ │ └── tab_item.xml │ │ │ ├── menu │ │ │ └── menu_main.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── add.png │ │ │ ├── back.png │ │ │ ├── cancel.png │ │ │ ├── delete.png │ │ │ ├── donation.png │ │ │ ├── feedback.png │ │ │ ├── full.png │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_black.png │ │ │ ├── ic_launcher_round.png │ │ │ ├── like.png │ │ │ ├── pause.png │ │ │ ├── play.png │ │ │ ├── reset.png │ │ │ ├── right.png │ │ │ ├── unlike.png │ │ │ └── update.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── values-zh │ │ │ └── strings.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ └── test │ │ └── java │ │ └── com │ │ └── z │ │ └── exoplayertest │ │ └── ExampleUnitTest.java └── tinkerpatch.gradle ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── image ├── test.txt ├── 微信图片_20200529163913.jpg ├── 微信图片_20200529163927.jpg ├── 微信图片_20200529163933.jpg └── 微信图片_20200529164235.jpg ├── settings.gradle └── 轻TV.1.0.0.apk /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExoPlayerTest 2 | 轻TV 简洁美观的电视直播软件,内置100多个频道,高清直播,支持手动添加频道。 3 | 4 | 基于exoplayer,采用RTMP协议开发,接入了腾讯行为搜集和腾讯bugly 5 | 6 | ![image](https://github.com/virtualC9/ExoPlayerTest/blob/master/image/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20200529163933.jpg) 7 | 8 | ![image](https://github.com/virtualC9/ExoPlayerTest/blob/master/image/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20200529164235.jpg) 9 | 10 | ![image](https://github.com/virtualC9/ExoPlayerTest/blob/master/image/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20200529163913.jpg) 11 | 12 | ![image](https://github.com/virtualC9/ExoPlayerTest/blob/master/image/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20200529163927.jpg) 13 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply from: 'tinkerpatch.gradle' 3 | 4 | android { 5 | compileSdkVersion 28 6 | buildToolsVersion "28.0.3" 7 | defaultConfig { 8 | applicationId "com.z.exoplayertest" 9 | minSdkVersion 21 10 | targetSdkVersion 28 11 | versionCode 100 12 | versionName "1.0.0" 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | 15 | ndk { 16 | //设置支持的SO库架构 17 | abiFilters 'armeabi' //, 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a' 18 | } 19 | manifestPlaceholders = [ 20 | //A8DGQ9DCZ22H 属于应用"轻TV"独有的 Android AppKey, 用于配置SDK 21 | MTA_APPKEY:"A8DGQ9DCZ22H", 22 | //标注应用推广渠道用以区分新用户来源,可填写如应用宝,豌豆荚等 23 | MTA_CHANNEL:"应用宝" 24 | ] 25 | } 26 | // 签名配置【buildTypes中调用了signingConfigs,则signingConfigs{}要置于buildTypes{}前面】 27 | signingConfigs { 28 | release { 29 | try { 30 | storeFile file("key.jks") 31 | storePassword "liu123456" 32 | keyAlias "key0" 33 | keyPassword "liu123456" 34 | } catch (ex) { 35 | throw new InvalidUserDataException(ex.toString()) 36 | } 37 | } 38 | } 39 | 40 | buildTypes { 41 | release { 42 | signingConfig signingConfigs.release 43 | minifyEnabled false 44 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 45 | } 46 | debug { 47 | signingConfig signingConfigs.release 48 | } 49 | } 50 | compileOptions { 51 | targetCompatibility JavaVersion.VERSION_1_8 52 | } 53 | 54 | repositories { 55 | google() 56 | jcenter() 57 | } 58 | 59 | repositories { 60 | flatDir { 61 | dirs 'libs' 62 | } 63 | } 64 | } 65 | 66 | dependencies { 67 | implementation fileTree(dir: 'libs', include: ['*.jar']) 68 | implementation 'androidx.appcompat:appcompat:1.1.0' 69 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 70 | implementation 'com.google.android.material:material:1.1.0' 71 | testImplementation 'junit:junit:4.12' 72 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 73 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 74 | 75 | implementation "com.google.android.exoplayer:exoplayer-core:${exoplayer_version}" 76 | implementation "com.google.android.exoplayer:exoplayer-dash:${exoplayer_version}" 77 | implementation "com.google.android.exoplayer:exoplayer-ui:${exoplayer_version}" 78 | implementation "com.google.android.exoplayer:extension-rtmp:${exoplayer_version}" 79 | implementation 'com.google.android.exoplayer:exoplayer-hls:2.10.1' 80 | implementation files('libs\\bugly_crashreport_upgrade-1.4.2.aar') 81 | //热修复 82 | implementation("com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.2.14.6") 83 | //MTA主包 84 | implementation 'com.qq.mta:mta:3.4.2' 85 | //MID基础包 86 | implementation 'com.tencent.mid:mid:3.73-release' 87 | // implementation 'com.tencent.bugly:crashreport_upgrade:latest.release' 88 | // implementation 'com.tencent.bugly:nativecrashreport:latest.release' 89 | // implementation("com.google.android.exoplayer:extension-rtmp:${exoplayer_version}") { 90 | // exclude group: 'net.butterflytv.utils', module: 'rtmp-client' 91 | // } 92 | // implementation "net.butterflytv.utils:rtmp-client:3.1.0" 93 | } 94 | -------------------------------------------------------------------------------- /app/libs/bugly_crashreport_upgrade-1.4.2.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/libs/bugly_crashreport_upgrade-1.4.2.aar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -keep public class com.tencent.bugly.**{*;} 23 | -keep class cn.jiajixin.nuwa.** { *; } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/z/exoplayertest/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | 25 | assertEquals("com.z.exoplayertest", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 43 | 48 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/assets/channel: -------------------------------------------------------------------------------- 1 | 香港财经&&rtmp://202.69.69.180:443/webcast/bshdlive-pc 2 | 湖南卫视高清&&rtmp://58.200.131.2:1935/livetv/hunanhd 3 | 广东卫视高清&&rtmp://58.200.131.2:1935/livetv/gdhd 4 | 东方卫视&&rtmp://58.200.131.2:1935/livetv/dftv 5 | 江苏卫视高清&&rtmp://58.200.131.2:1935/livetv/jshd 6 | 浙江卫视高清&&rtmp://58.200.131.2:1935/livetv/zjhd 7 | CCTV-1HD&&rtmp://58.200.131.2:1935/livetv/cctv1hd 8 | CCTV-2HD&&rtmp://58.200.131.2:1935/livetv/cctv2hd 9 | CCTV-3HD&&rtmp://58.200.131.2:1935/livetv/cctv3hd 10 | CCTV-4HD&&rtmp://58.200.131.2:1935/livetv/cctv4hd 11 | CCTV-5HD&&rtmp://58.200.131.2:1935/livetv/cctv5hd 12 | CCTV5+HD&&rtmp://58.200.131.2:1935/livetv/cctv5phd 13 | CCTV-6HD&&rtmp://58.200.131.2:1935/livetv/cctv6hd 14 | CCTV-7HD&&rtmp://58.200.131.2:1935/livetv/cctv7hd 15 | CCTV-8HD&&rtmp://58.200.131.2:1935/livetv/cctv8hd 16 | CCTV-9HD&&rtmp://58.200.131.2:1935/livetv/cctv9hd 17 | CCTV-10HD&&rtmp://58.200.131.2:1935/livetv/cctv10hd 18 | CCTV-12HD&&rtmp://58.200.131.2:1935/livetv/cctv12hd 19 | CCTV-14HD&&rtmp://58.200.131.2:1935/livetv/cctv14hd 20 | CCTV-1综合&&rtmp://58.200.131.2:1935/livetv/cctv1 21 | CCTV-2财经&&rtmp://58.200.131.2:1935/livetv/cctv2 22 | CCTV-3综艺&&rtmp://58.200.131.2:1935/livetv/cctv3 23 | CCTV-4中文国际&&rtmp://58.200.131.2:1935/livetv/cctv4 24 | CCTV-5体育&&rtmp://58.200.131.2:1935/livetv/cctv5 25 | CCTV-6电影&&rtmp://58.200.131.2:1935/livetv/cctv6 26 | CCTV-7军事农业&&rtmp://58.200.131.2:1935/livetv/cctv7 27 | CCTV-8电视剧&&rtmp://58.200.131.2:1935/livetv/cctv8 28 | CCTV-9记录&&rtmp://58.200.131.2:1935/livetv/cctv9 29 | CCTV-10科教&&rtmp://58.200.131.2:1935/livetv/cctv10 30 | CCTV-11戏曲&&rtmp://58.200.131.2:1935/livetv/cctv11 31 | CCTV-12社会与法&&rtmp://58.200.131.2:1935/livetv/cctv12 32 | CCTV-13新闻&&rtmp://58.200.131.2:1935/livetv/cctv13 33 | CCTV-14少儿&&rtmp://58.200.131.2:1935/livetv/cctv14 34 | CCTV-15音乐&&rtmp://58.200.131.2:1935/livetv/cctv15 35 | CGTN-新闻&&rtmp://58.200.131.2:1935/livetv/cctv16 36 | CETV-1&&rtmp://58.200.131.2:1935/livetv/cetv1 37 | CETV-3&&rtmp://58.200.131.2:1935/livetv/cetv3 38 | CETV-4&&rtmp://58.200.131.2:1935/livetv/cetv4 39 | 北京卫视高清&&rtmp://58.200.131.2:1935/livetv/btv1hd 40 | 北京影视高清&&rtmp://58.200.131.2:1935/livetv/btv4hd 41 | 北京体育高清&&rtmp://58.200.131.2:1935/livetv/btv6hd 42 | 北京新闻高清&&rtmp://58.200.131.2:1935/livetv/btv9hd 43 | 北京纪实高清&&rtmp://58.200.131.2:1935/livetv/btv11hd 44 | 北京卫视&&rtmp://58.200.131.2:1935/livetv/btv1 45 | 北京文艺&&rtmp://58.200.131.2:1935/livetv/btv2 46 | 北京科教&&rtmp://58.200.131.2:1935/livetv/btv3 47 | 北京影视&&rtmp://58.200.131.2:1935/livetv/btv4 48 | 北京财经&&rtmp://58.200.131.2:1935/livetv/btv5 49 | 北京体育&&rtmp://58.200.131.2:1935/livetv/btv6 50 | 北京生活&&rtmp://58.200.131.2:1935/livetv/btv7 51 | 北京青年&&rtmp://58.200.131.2:1935/livetv/btv8 52 | 北京新闻&&rtmp://58.200.131.2:1935/livetv/btv9 53 | 北京卡酷&&rtmp://58.200.131.2:1935/livetv/btv10 54 | 北京文艺高清&&rtmp://58.200.131.2:1935/livetv/btv2hd 55 | 安徽卫视高清&&rtmp://58.200.131.2:1935/livetv/ahhd 56 | 重庆卫视高清&&rtmp://58.200.131.2:1935/livetv/cqhd 57 | 东方卫视高清&&rtmp://58.200.131.2:1935/livetv/dfhd 58 | 天津卫视高清&&rtmp://58.200.131.2:1935/livetv/tjhd 59 | 东南卫视高清&&rtmp://58.200.131.2:1935/livetv/dnhd 60 | 江西卫视高清&&rtmp://58.200.131.2:1935/livetv/jxhd 61 | 河北卫视高清&&rtmp://58.200.131.2:1935/livetv/hebhd 62 | 湖北卫视高清&&rtmp://58.200.131.2:1935/livetv/hbhd 63 | 辽宁卫视高清&&rtmp://58.200.131.2:1935/livetv/lnhd 64 | 四川卫视高清&&rtmp://58.200.131.2:1935/livetv/schd 65 | 山东卫视高清&&rtmp://58.200.131.2:1935/livetv/sdhd 66 | 深圳卫视高清&&rtmp://58.200.131.2:1935/livetv/szhd 67 | 黑龙江卫视高清&&rtmp://58.200.131.2:1935/livetv/hljhd 68 | CHC高清电影&&rtmp://58.200.131.2:1935/livetv/chchd 69 | 上海纪实高清&&rtmp://58.200.131.2:1935/livetv/docuchina 70 | 金鹰纪实高清&&rtmp://58.200.131.2:1935/livetv/gedocu 71 | 全纪实高清&&rtmp://58.200.131.2:1935/livetv/documentaryhd 72 | CCTV-第一剧场&&rtmp://58.200.131.2:1935/livetv/dyjctv 73 | CCTV-国防军事&&rtmp://58.200.131.2:1935/livetv/gfjstv 74 | CCTV-怀旧剧场&&rtmp://58.200.131.2:1935/livetv/hjjctv 75 | CCTV-风云剧场&&rtmp://58.200.131.2:1935/livetv/fyjctv 76 | CCTV-风云足球&&rtmp://58.200.131.2:1935/livetv/fyzqtv 77 | CCTV-风云音乐&&rtmp://58.200.131.2:1935/livetv/fyyytv 78 | CCTV-世界地理&&rtmp://58.200.131.2:1935/livetv/sjdltv 79 | 凤凰卫视中文台&&rtmp://58.200.131.2:1935/livetv/fhzw 80 | 凤凰卫视资讯台&&rtmp://58.200.131.2:1935/livetv/fhzx 81 | 凤凰卫视电影台&&rtmp://58.200.131.2:1935/livetv/fhdy 82 | 星空卫视&&rtmp://58.200.131.2:1935/livetv/startv 83 | Star Sports&&rtmp://58.200.131.2:1935/livetv/starsports 84 | Channel[V]&&rtmp://58.200.131.2:1935/livetv/channelv 85 | 探索频道&&rtmp://58.200.131.2:1935/livetv/discovery 86 | 国家地理频道&&rtmp://58.200.131.2:1935/livetv/natlgeo 87 | CHC家庭影院&&rtmp://58.200.131.2:1935/livetv/chctv 88 | CHC动作电影&&rtmp://58.200.131.2:1935/livetv/chcatv 89 | 安徽卫视&&rtmp://58.200.131.2:1935/livetv/ahtv 90 | 兵团卫视&&rtmp://58.200.131.2:1935/livetv/bttv 91 | 重庆卫视&&rtmp://58.200.131.2:1935/livetv/cqtv 92 | 东南卫视&&rtmp://58.200.131.2:1935/livetv/dntv 93 | 广东卫视&&rtmp://58.200.131.2:1935/livetv/gdtv 94 | 广西卫视&&rtmp://58.200.131.2:1935/livetv/gxtv 95 | 甘肃卫视&&rtmp://58.200.131.2:1935/livetv/gstv 96 | 贵州卫视&&rtmp://58.200.131.2:1935/livetv/gztv 97 | 湖北卫视&&rtmp://58.200.131.2:1935/livetv/hbtv 98 | 湖南卫视&&rtmp://58.200.131.2:1935/livetv/hunantv 99 | 河北卫视&&rtmp://58.200.131.2:1935/livetv/hebtv 100 | 河南卫视&&rtmp://58.200.131.2:1935/livetv/hntv 101 | 黑龙江卫视&&rtmp://58.200.131.2:1935/livetv/hljtv 102 | 江苏卫视&&rtmp://58.200.131.2:1935/livetv/jstv 103 | 江西卫视&&rtmp://58.200.131.2:1935/livetv/jxtv 104 | 吉林卫视&&rtmp://58.200.131.2:1935/livetv/jltv 105 | 辽宁卫视&&rtmp://58.200.131.2:1935/livetv/lntv 106 | 内蒙古卫视&&rtmp://58.200.131.2:1935/livetv/nmtv 107 | 宁夏卫视&&rtmp://58.200.131.2:1935/livetv/nxtv 108 | 青海卫视&&rtmp://58.200.131.2:1935/livetv/qhtv 109 | 四川卫视&&rtmp://58.200.131.2:1935/livetv/sctv 110 | 山东卫视&&rtmp://58.200.131.2:1935/livetv/sdtv 111 | 山西卫视&&rtmp://58.200.131.2:1935/livetv/sxrtv 112 | 陕西卫视&&rtmp://58.200.131.2:1935/livetv/sxtv 113 | 山东教育&&rtmp://58.200.131.2:1935/livetv/sdetv 114 | 中国教育-1&&rtmp://58.200.131.2:1935/livetv/cetv1 115 | 中国教育-3&&rtmp://58.200.131.2:1935/livetv/cetv3 116 | 中国教育-4&&rtmp://58.200.131.2:1935/livetv/cetv4 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/adpter/ChannelAdaptr.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.adpter; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ImageView; 8 | import android.widget.TextView; 9 | 10 | import com.z.exoplayertest.R; 11 | import com.z.exoplayertest.database.Channel; 12 | 13 | import java.util.List; 14 | 15 | import androidx.annotation.NonNull; 16 | import androidx.recyclerview.widget.RecyclerView; 17 | 18 | public class ChannelAdaptr extends RecyclerView.Adapter { 19 | 20 | //数据源 21 | private List mList; 22 | private OnItemClickListener onItemClickListener; 23 | private Context context; 24 | 25 | public ChannelAdaptr(Context context,List list) { 26 | mList = list; 27 | this.context = context; 28 | } 29 | 30 | //返回item个数 31 | @Override 32 | public int getItemCount() { 33 | if (mList == null || mList.size() == 0) { 34 | return 0; 35 | } 36 | return mList.size(); 37 | } 38 | 39 | //创建ViewHolder 40 | @NonNull 41 | @Override 42 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 43 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.channel_item, parent, false); 44 | ViewHolder viewHolder = new ViewHolder(view); 45 | return viewHolder; 46 | } 47 | 48 | //填充视图 49 | @Override 50 | public void onBindViewHolder(@NonNull final ChannelAdaptr.ViewHolder holder, final int position) { 51 | holder.mView.setText(mList.get(position).getName()); 52 | if (mList.get(position).getIsLike() == 1) { 53 | holder.imageView.setImageDrawable(context.getDrawable(R.mipmap.like)); 54 | }else { 55 | holder.imageView.setImageDrawable(context.getDrawable(R.mipmap.unlike)); 56 | } 57 | //添加点击监听事件 58 | holder.itemView.setOnClickListener(new View.OnClickListener() { 59 | @Override 60 | public void onClick(View v) { 61 | onItemClickListener.onClickItem(position, mList.get(position), false); 62 | } 63 | }); 64 | holder.imageView.setOnClickListener(new View.OnClickListener() { 65 | @Override 66 | public void onClick(View v) { 67 | onItemClickListener.onClickItem(position, mList.get(position), true); 68 | } 69 | }); 70 | } 71 | 72 | public class ViewHolder extends RecyclerView.ViewHolder { 73 | public TextView mView; 74 | public ImageView imageView; 75 | 76 | public ViewHolder(View itemView) { 77 | super(itemView); 78 | mView = itemView.findViewById(R.id.channel_name); 79 | imageView = itemView.findViewById(R.id.item_like); 80 | } 81 | } 82 | 83 | /** 84 | * 接口 85 | */ 86 | public interface OnItemClickListener { 87 | /** 88 | * 点击标签 89 | * 90 | * @param position 91 | * @param channel 92 | * @param isClickLike 93 | */ 94 | void onClickItem(int position, Channel channel, boolean isClickLike); 95 | } 96 | 97 | public void setOnItemClickListener(OnItemClickListener onItemClickListener) { 98 | this.onItemClickListener = onItemClickListener; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/adpter/LikeAdaptr.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.adpter; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ImageView; 8 | import android.widget.TextView; 9 | 10 | import com.z.exoplayertest.R; 11 | import com.z.exoplayertest.database.Channel; 12 | 13 | import java.util.List; 14 | 15 | import androidx.annotation.NonNull; 16 | import androidx.recyclerview.widget.RecyclerView; 17 | 18 | public class LikeAdaptr extends RecyclerView.Adapter { 19 | 20 | //数据源 21 | private List mList; 22 | private OnItemClickListener onItemClickListener; 23 | private Context context; 24 | 25 | public LikeAdaptr(Context context, List list) { 26 | mList = list; 27 | this.context = context; 28 | } 29 | 30 | //返回item个数 31 | @Override 32 | public int getItemCount() { 33 | if (mList == null || mList.size() == 0) { 34 | return 0; 35 | } 36 | return mList.size(); 37 | } 38 | 39 | //创建ViewHolder 40 | @NonNull 41 | @Override 42 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 43 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.channel_item, parent, false); 44 | ViewHolder viewHolder = new ViewHolder(view); 45 | return viewHolder; 46 | } 47 | 48 | //填充视图 49 | @Override 50 | public void onBindViewHolder(@NonNull final LikeAdaptr.ViewHolder holder, final int position) { 51 | holder.mView.setText(mList.get(position).getName()); 52 | holder.imageView.setImageDrawable(context.getDrawable(R.mipmap.delete)); 53 | //添加点击监听事件 54 | holder.itemView.setOnClickListener(new View.OnClickListener() { 55 | @Override 56 | public void onClick(View v) { 57 | onItemClickListener.onClickItem(position, mList.get(position), false); 58 | } 59 | }); 60 | holder.imageView.setOnClickListener(new View.OnClickListener() { 61 | @Override 62 | public void onClick(View v) { 63 | onItemClickListener.onClickItem(position, mList.get(position), true); 64 | } 65 | }); 66 | } 67 | 68 | public class ViewHolder extends RecyclerView.ViewHolder { 69 | public TextView mView; 70 | public ImageView imageView; 71 | 72 | public ViewHolder(View itemView) { 73 | super(itemView); 74 | mView = itemView.findViewById(R.id.channel_name); 75 | imageView = itemView.findViewById(R.id.item_like); 76 | } 77 | } 78 | 79 | /** 80 | * 接口 81 | */ 82 | public interface OnItemClickListener { 83 | /** 84 | * 点击标签 85 | * 86 | * @param position 87 | * @param channel 88 | * @param isClickLike 89 | */ 90 | void onClickItem(int position, Channel channel, boolean isClickLike); 91 | } 92 | 93 | public void setOnItemClickListener(OnItemClickListener onItemClickListener) { 94 | this.onItemClickListener = onItemClickListener; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/adpter/PagerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.adpter; 2 | 3 | import android.content.Context; 4 | 5 | import com.z.exoplayertest.view.ChannelFragment; 6 | import com.z.exoplayertest.view.LikeFragment; 7 | import com.z.exoplayertest.view.SettingFragment; 8 | 9 | import androidx.fragment.app.Fragment; 10 | import androidx.fragment.app.FragmentManager; 11 | import androidx.fragment.app.FragmentPagerAdapter; 12 | 13 | public class PagerAdapter extends FragmentPagerAdapter { 14 | private static final int PAGE_COUNT = 3; 15 | 16 | public PagerAdapter(Context context, FragmentManager fm) { 17 | super(fm); 18 | } 19 | 20 | @Override 21 | public Fragment getItem(int position) { 22 | switch (position) { 23 | case 0: 24 | return ChannelFragment.newInstance(); 25 | case 1: 26 | return LikeFragment.newInstance(); 27 | case 2: 28 | return SettingFragment.newInstance(); 29 | default: 30 | return ChannelFragment.newInstance(); 31 | } 32 | 33 | } 34 | 35 | @Override 36 | public int getCount() { 37 | return PAGE_COUNT; 38 | } 39 | 40 | @Override 41 | public CharSequence getPageTitle(int position) { 42 | switch (position) { 43 | case 0: 44 | return "频道"; 45 | case 1: 46 | return "收藏"; 47 | case 2: 48 | return "设置"; 49 | default: 50 | return ""; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/database/Channel.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.database; 2 | 3 | public class Channel { 4 | private int id; 5 | private String name; 6 | private String url; 7 | private int isLike; 8 | 9 | public Channel(int id, String name, String url, int isLike) { 10 | this.id = id; 11 | this.name = name; 12 | this.url = url; 13 | this.isLike = isLike; 14 | } 15 | 16 | public Channel(String name, String url, int isLike) { 17 | this.name = name; 18 | this.url = url; 19 | this.isLike = isLike; 20 | } 21 | 22 | public int getId() { 23 | return id; 24 | } 25 | 26 | public void setId(int id) { 27 | this.id = id; 28 | } 29 | 30 | public String getName() { 31 | return name; 32 | } 33 | 34 | public void setName(String name) { 35 | this.name = name; 36 | } 37 | 38 | public String getUrl() { 39 | return url; 40 | } 41 | 42 | public void setUrl(String url) { 43 | this.url = url; 44 | } 45 | 46 | public int getIsLike() { 47 | return isLike; 48 | } 49 | 50 | public void setIsLike(int isLike) { 51 | this.isLike = isLike; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/database/ChannelDao.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.database; 2 | 3 | import android.content.Context; 4 | import android.database.Cursor; 5 | import android.database.sqlite.SQLiteDatabase; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class ChannelDao { 11 | private SqliteHelper helper; 12 | private static ChannelDao instance = null; 13 | private SQLiteDatabase db; 14 | 15 | public ChannelDao(Context context) { 16 | helper = new SqliteHelper(context); 17 | db = helper.getWritableDatabase(); 18 | } 19 | 20 | public static ChannelDao getInstance(Context context) { 21 | if (instance == null) { 22 | instance = new ChannelDao(context); 23 | } 24 | return instance; 25 | } 26 | 27 | /** 28 | * 插入数据 29 | * 30 | * @param channel 31 | */ 32 | public void add(Channel channel) { 33 | String sql = "Insert into Channel(name,url,isLike) values(?,?,?)"; 34 | db.execSQL(sql, new Object[]{channel.getName(), channel.getUrl(), channel.getIsLike() 35 | } 36 | ); 37 | } 38 | 39 | /** 40 | * 插入数据 41 | * 42 | * @param list 43 | */ 44 | public void addList(List list) { 45 | if (list == null || list.size() < 1) { 46 | return; 47 | } 48 | String sql = "Insert into Channel(name,url,isLike) values(?,?,?)"; 49 | for (Channel channel : list) { 50 | db.execSQL(sql, new Object[]{channel.getName(), channel.getUrl(), channel.getIsLike()}); 51 | } 52 | } 53 | 54 | public void deleteAll(){ 55 | db.execSQL("delete from Channel"); 56 | } 57 | /** 58 | * 修改数据 59 | */ 60 | public void update(String[] values) { 61 | db.execSQL("update Channel set isLike=? where id=? ", values); 62 | } 63 | 64 | 65 | /** 66 | * 获取所有数据 67 | * 68 | * @return 69 | */ 70 | public List getAllData() { 71 | List channelList = new ArrayList(); 72 | String sql = "select * from Channel"; 73 | Cursor cursor = db.rawQuery(sql, new String[]{ 74 | 75 | }); 76 | while (cursor.moveToNext()) { 77 | channelList.add(new Channel(cursor.getInt(cursor.getColumnIndex("id")), 78 | cursor.getString(cursor.getColumnIndex("name")), 79 | cursor.getString(cursor.getColumnIndex("url")), 80 | cursor.getInt(cursor.getColumnIndex("isLike")) 81 | ) 82 | ); 83 | } 84 | return channelList; 85 | } 86 | 87 | /** 88 | * 根据id查询 89 | * 90 | * @param id 91 | * @return 92 | */ 93 | public Channel findById(int id) { 94 | String sql = "select id,name,url,isLike from Channel where id=?"; 95 | Cursor cursor = db.rawQuery(sql, new String[]{ 96 | String.valueOf(id) 97 | }); 98 | if (cursor.moveToNext()) { 99 | return new Channel( 100 | cursor.getInt(cursor.getColumnIndex("id")), 101 | cursor.getString(cursor.getColumnIndex("name")), 102 | cursor.getString(cursor.getColumnIndex("url")), 103 | cursor.getInt(cursor.getColumnIndex("isLike")) 104 | ); 105 | } 106 | return null; 107 | } 108 | 109 | /** 110 | * 获取用户收藏 111 | * 112 | * @return 113 | */ 114 | public List queryUserLike() { 115 | List channelList = new ArrayList(); 116 | 117 | String sql = "select * from Channel where isLike = ?"; 118 | Cursor cursor = db.rawQuery(sql, new String[]{"1"}); 119 | while (cursor.moveToNext()) { 120 | channelList.add(new Channel(cursor.getInt(cursor.getColumnIndex("id")), 121 | cursor.getString(cursor.getColumnIndex("name")), 122 | cursor.getString(cursor.getColumnIndex("url")), 123 | cursor.getInt(cursor.getColumnIndex("isLike")) 124 | ) 125 | ); 126 | } 127 | return channelList; 128 | } 129 | 130 | /** 131 | * 根据名称模糊查询用户收藏 132 | * 133 | * @return 134 | */ 135 | public List queryByNameAndLike(String value) { 136 | List channelList = new ArrayList(); 137 | 138 | String sql = "select * from Channel where name like ? and isLike = ?"; 139 | Cursor cursor = db.rawQuery(sql, new String[]{"%" + value + "%","1"}); 140 | while (cursor.moveToNext()) { 141 | channelList.add(new Channel(cursor.getInt(cursor.getColumnIndex("id")), 142 | cursor.getString(cursor.getColumnIndex("name")), 143 | cursor.getString(cursor.getColumnIndex("url")), 144 | cursor.getInt(cursor.getColumnIndex("isLike")) 145 | ) 146 | ); 147 | } 148 | return channelList; 149 | } 150 | 151 | /** 152 | * 根据名称模糊查询 153 | * 154 | * @return 155 | */ 156 | public List queryByName(String value) { 157 | List channelList = new ArrayList(); 158 | 159 | String sql = "select * from Channel where name like ?"; 160 | Cursor cursor = db.rawQuery(sql, new String[]{"%" + value + "%"}); 161 | while (cursor.moveToNext()) { 162 | channelList.add(new Channel(cursor.getInt(cursor.getColumnIndex("id")), 163 | cursor.getString(cursor.getColumnIndex("name")), 164 | cursor.getString(cursor.getColumnIndex("url")), 165 | cursor.getInt(cursor.getColumnIndex("isLike")) 166 | ) 167 | ); 168 | } 169 | return channelList; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/database/SqliteHelper.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.database; 2 | 3 | import android.content.Context; 4 | import android.database.sqlite.SQLiteDatabase; 5 | import android.database.sqlite.SQLiteOpenHelper; 6 | 7 | public class SqliteHelper extends SQLiteOpenHelper { 8 | 9 | /** 10 | * 数据库版本 11 | */ 12 | private static final int DB_VERSION = 1; 13 | /** 14 | * 数据库名称 15 | */ 16 | private static final String DB_NAME = "TvShow.db"; 17 | /** 18 | * 表的名称 19 | */ 20 | public static final String TABLE_NAME = "Channel"; 21 | 22 | public SqliteHelper(Context context) { 23 | super(context, DB_NAME, null, DB_VERSION); 24 | } 25 | 26 | /** 27 | * 这个方法 28 | * 1、在第一次打开数据库的时候才会走 29 | * 2、在清除数据之后再次运行-->打开数据库,这个方法会走 30 | * 3、没有清除数据,不会走这个方法 31 | * 4、数据库升级的时候这个方法不会走 32 | */ 33 | @Override 34 | public void onCreate(SQLiteDatabase db) { 35 | //初始化数据表,可以再这里面对多个表进行处理 36 | String sql = "create table if not exists " + TABLE_NAME + " (id integer primary key AUTOINCREMENT, name text, url text, isLike integer)"; 37 | db.execSQL(sql); 38 | } 39 | 40 | /** 41 | * 数据库升级 42 | * 1、第一次创建数据库的时候,这个方法不会走 43 | * 2、清除数据后再次运行(相当于第一次创建)这个方法不会走 44 | * 3、数据库已经存在,而且版本升高的时候,这个方法才会调用 45 | * 46 | * @param db 47 | * @param oldVersion 48 | * @param newVersion 49 | */ 50 | @Override 51 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 52 | String sql = "DROP TABLE IF EXISTS " + TABLE_NAME; 53 | db.execSQL(sql); 54 | onCreate(db); 55 | } 56 | 57 | /** 58 | * 执行数据库的降级操作 59 | * 1、只有新版本比旧版本低的时候才会执行 60 | * 2、如果不执行降级操作,会抛出异常 61 | */ 62 | @Override 63 | public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { 64 | super.onDowngrade(db, oldVersion, newVersion); 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/utils/AppUtils.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.utils; 2 | 3 | import android.content.Context; 4 | import android.util.DisplayMetrics; 5 | 6 | /** 7 | * Created by Raul_lsj on 2018/3/5. 8 | */ 9 | 10 | public class AppUtils { 11 | 12 | public final static String WIDTH = "width"; 13 | 14 | public final static String HEIGHT = "height"; 15 | 16 | /** 17 | * px转dp 18 | * 19 | * @param context The context 20 | * @param px the pixel value 21 | * @return value in dp 22 | */ 23 | public static int pxToDp(Context context, float px) { 24 | DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); 25 | return (int) ((px / displayMetrics.density) + 0.5f); 26 | } 27 | 28 | /** 29 | * dp转px 30 | * 31 | * @param context 32 | * @param dp 33 | * @return 34 | */ 35 | public static int dpToPx(Context context, float dp) { 36 | DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); 37 | return (int) ((dp * displayMetrics.density) + 0.5f); 38 | } 39 | 40 | /** 41 | * 获取状态栏高度 42 | * 43 | * @param context 44 | * @return 45 | */ 46 | public static int getStatusBarHeight(Context context) { 47 | int result = 0; 48 | int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", 49 | "android"); 50 | if (resourceId > 0) { 51 | result = context.getResources().getDimensionPixelSize(resourceId); 52 | } 53 | return result; 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/utils/Density.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.utils; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.content.ComponentCallbacks; 6 | import android.content.res.Configuration; 7 | import android.util.DisplayMetrics; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.Nullable; 11 | 12 | /** 13 | * 通过修改系统参数来适配android设备 14 | * https://www.jianshu.com/p/4254ea9d1b27 15 | *

16 | * Created by Raul_lsj on 2018/6/6. 17 | */ 18 | 19 | public class Density { 20 | 21 | private static float appDensity; 22 | private static float appScaledDensity; 23 | private static DisplayMetrics appDisplayMetrics; 24 | private static int barHeight; 25 | 26 | public static void setDensity(@NonNull final Application application) { 27 | //获取application的DisplayMetrics 28 | appDisplayMetrics = application.getResources().getDisplayMetrics(); 29 | //获取状态栏高度 30 | barHeight = AppUtils.getStatusBarHeight(application); 31 | 32 | if (appDensity == 0) { 33 | //初始化的时候赋值 34 | appDensity = appDisplayMetrics.density; 35 | appScaledDensity = appDisplayMetrics.scaledDensity; 36 | 37 | //添加字体变化的监听 38 | application.registerComponentCallbacks(new ComponentCallbacks() { 39 | @Override 40 | public void onConfigurationChanged(Configuration newConfig) { 41 | //字体改变后,将appScaledDensity重新赋值 42 | if (newConfig != null && newConfig.fontScale > 0) { 43 | appScaledDensity = application.getResources().getDisplayMetrics().scaledDensity; 44 | } 45 | } 46 | 47 | @Override 48 | public void onLowMemory() { 49 | } 50 | }); 51 | } 52 | } 53 | 54 | //此方法在BaseActivity中做初始化(如果不封装BaseActivity的话,直接用下面那个方法就好) 55 | public static void setDefault(Activity activity) { 56 | setAppOrientation(activity, AppUtils.WIDTH); 57 | } 58 | 59 | //此方法用于在某一个Activity里面更改适配的方向 60 | public static void setOrientation(Activity activity, String orientation) { 61 | setAppOrientation(activity, orientation); 62 | } 63 | 64 | /** 65 | * targetDensity 66 | * targetScaledDensity 67 | * targetDensityDpi 68 | * 这三个参数是统一修改过后的值 69 | *

70 | * orientation:方向值,传入width或height 71 | */ 72 | private static void setAppOrientation(@Nullable Activity activity, String orientation) { 73 | 74 | float targetDensity; 75 | 76 | if (orientation.equals("height")) { 77 | targetDensity = (appDisplayMetrics.heightPixels - barHeight) / 667f; 78 | } else { 79 | targetDensity = appDisplayMetrics.widthPixels / 360f; 80 | } 81 | 82 | float targetScaledDensity = targetDensity * (appScaledDensity / appDensity); 83 | int targetDensityDpi = (int) (160 * targetDensity); 84 | 85 | /** 86 | * 87 | * 最后在这里将修改过后的值赋给系统参数 88 | * 89 | * 只修改Activity的density值 90 | */ 91 | DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics(); 92 | activityDisplayMetrics.density = targetDensity; 93 | activityDisplayMetrics.scaledDensity = targetScaledDensity; 94 | activityDisplayMetrics.densityDpi = targetDensityDpi; 95 | } 96 | } -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/utils/FileUtil.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.utils; 2 | 3 | import android.content.Context; 4 | import android.content.res.AssetManager; 5 | import android.os.Environment; 6 | 7 | import com.z.exoplayertest.database.Channel; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.BufferedWriter; 11 | import java.io.File; 12 | import java.io.FileInputStream; 13 | import java.io.FileOutputStream; 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.io.InputStreamReader; 17 | import java.io.OutputStreamWriter; 18 | import java.io.RandomAccessFile; 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | public class FileUtil { 23 | /** 24 | * 用户手动添加频道文件地址 25 | */ 26 | public static String USER_CHANNEL_FILE_PATH = Environment.getExternalStorageDirectory() + File.separator + "channel.txt"; 27 | /** 28 | * 获取文件中的数据 29 | * 30 | * @param context 31 | * @return 32 | */ 33 | static public List getChannelFromTxt(Context context) { 34 | List list = new ArrayList<>(); 35 | AssetManager am = context.getAssets(); 36 | try { 37 | //读取assets内的文件 38 | InputStream is = am.open("channel"); 39 | BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is, "utf-8")); 40 | 41 | String lineTxt = null; 42 | while ((lineTxt = bufferedReader.readLine()) != null) { 43 | if (lineTxt.contains("&&")) { 44 | String[] str = lineTxt.split("&&"); 45 | if (str.length > 1) { 46 | list.add(new Channel(str[0], str[1], 0)); 47 | } 48 | } 49 | } 50 | bufferedReader.close(); 51 | is.close(); 52 | 53 | File file = new File(USER_CHANNEL_FILE_PATH); 54 | if (file.exists()) { 55 | //读取用户手动添加的文件 56 | InputStreamReader reader = new InputStreamReader(new FileInputStream(file), "utf-8"); 57 | BufferedReader br = new BufferedReader(reader); 58 | String s = null; 59 | while ((s = br.readLine()) != null) { 60 | if (s.contains("&&")) { 61 | String[] str = s.split("&&"); 62 | if (str.length > 1) { 63 | list.add(new Channel(str[0], str[1], 0)); 64 | } 65 | } 66 | } 67 | } 68 | } catch (IOException e) { 69 | e.printStackTrace(); 70 | } 71 | return list; 72 | } 73 | 74 | // 将字符串写入到文本文件中 75 | public static void writeFile(String strFilePath, String strcontent) { 76 | // 每次写入时,都换行写 77 | String strContent = strcontent + "\r\n"; 78 | try { 79 | File file = new File(strFilePath); 80 | if (!file.exists()) { 81 | file.getParentFile().mkdirs(); 82 | file.createNewFile(); 83 | } 84 | RandomAccessFile raf = new RandomAccessFile(file, "rwd"); 85 | raf.seek(file.length()); 86 | raf.write(strContent.getBytes()); 87 | raf.close(); 88 | } catch (Exception e) { 89 | 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/view/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.view; 2 | 3 | import android.os.Bundle; 4 | import android.view.Window; 5 | import android.view.WindowManager; 6 | import android.widget.Toast; 7 | 8 | import androidx.annotation.Nullable; 9 | import androidx.appcompat.app.AppCompatActivity; 10 | 11 | public abstract class BaseActivity extends AppCompatActivity { 12 | /***是否显示标题栏*/ 13 | private boolean isshowtitle = false; 14 | /***是否显示标题栏*/ 15 | private boolean isshowstate = true; 16 | /***封装toast对象**/ 17 | private static Toast toast; 18 | /***获取TAG的activity名称**/ 19 | protected final String TAG = this.getClass().getSimpleName(); 20 | @Override 21 | protected void onCreate(@Nullable Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | if(!isshowtitle){ 24 | requestWindowFeature(Window.FEATURE_NO_TITLE); 25 | } 26 | 27 | if(isshowstate){ 28 | getWindow().setFlags(WindowManager.LayoutParams. FLAG_FULLSCREEN , 29 | WindowManager.LayoutParams. FLAG_FULLSCREEN); 30 | } 31 | //设置布局 32 | setContentView(intiLayout()); 33 | //初始化控件 34 | initView(); 35 | //设置数据 36 | initData(); 37 | } 38 | 39 | /** 40 | * 设置布局 41 | * 42 | * @return 43 | */ 44 | public abstract int intiLayout(); 45 | 46 | /** 47 | * 初始化布局 48 | */ 49 | public abstract void initView(); 50 | 51 | /** 52 | * 设置数据 53 | */ 54 | public abstract void initData(); 55 | 56 | /** 57 | * 是否设置标题栏 58 | * 59 | * @return 60 | */ 61 | public void setTitle(boolean ishow) { 62 | isshowtitle=ishow; 63 | } 64 | 65 | /** 66 | * 设置是否显示状态栏 67 | * @param ishow 68 | */ 69 | public void setState(boolean ishow) { 70 | isshowstate=ishow; 71 | } 72 | 73 | /** 74 | * 显示长toast 75 | * @param msg 76 | */ 77 | public void toastLong(String msg){ 78 | if (null == toast) { 79 | toast = new Toast(this); 80 | toast.setDuration(Toast.LENGTH_LONG); 81 | toast.setText(msg); 82 | toast.show(); 83 | } else { 84 | toast.setText(msg); 85 | toast.show(); 86 | } 87 | } 88 | 89 | /** 90 | * 显示短toast 91 | * @param msg 92 | */ 93 | public void toastShort(String msg){ 94 | if (null == toast) { 95 | toast = new Toast(this); 96 | toast.setDuration(Toast.LENGTH_SHORT); 97 | toast.setText(msg); 98 | toast.show(); 99 | } else { 100 | toast.setText(msg); 101 | toast.show(); 102 | } 103 | } 104 | 105 | } -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/view/BaseApplication.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.view; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import com.tencent.bugly.Bugly; 7 | import com.tencent.stat.StatConfig; 8 | import com.tencent.stat.StatService; 9 | import com.tencent.tinker.entry.ApplicationLike; 10 | import com.tinkerpatch.sdk.TinkerPatch; 11 | import com.tinkerpatch.sdk.loader.TinkerPatchApplicationLike; 12 | import com.z.exoplayertest.BuildConfig; 13 | import com.z.exoplayertest.database.Channel; 14 | import com.z.exoplayertest.database.ChannelDao; 15 | import com.z.exoplayertest.utils.Density; 16 | import com.z.exoplayertest.utils.FileUtil; 17 | 18 | import java.util.List; 19 | 20 | public class BaseApplication extends Application { 21 | private ApplicationLike tinkerApplicationLike; 22 | @Override 23 | public void onCreate() { 24 | super.onCreate(); 25 | Bugly.init(getApplicationContext(), "ff995c6793", false); 26 | Density.setDensity(this); 27 | ChannelDao channelDao = ChannelDao.getInstance(this); 28 | List list = channelDao.getAllData(); 29 | if (list == null || list.size() < 1) { 30 | channelDao.addList(FileUtil.getChannelFromTxt(this)); 31 | } 32 | // [可选]设置是否打开debug输出,上线时请关闭,Logcat标签为"MtaSDK" 33 | StatConfig.setDebugEnable(false); 34 | // 基础统计API 35 | StatService.registerActivityLifecycleCallbacks(this); 36 | 37 | if (BuildConfig.TINKER_ENABLE) { 38 | // 我们可以从这里获得Tinker加载过程的信息 39 | tinkerApplicationLike = TinkerPatchApplicationLike.getTinkerPatchApplicationLike(); 40 | 41 | // 初始化TinkerPatch SDK, 更多配置可参照API章节中的,初始化SDK 42 | TinkerPatch.init(tinkerApplicationLike) 43 | .reflectPatchLibrary() 44 | .setPatchRollbackOnScreenOff(true) 45 | .setPatchRestartOnSrceenOff(true) 46 | .setFetchPatchIntervalByHours(3); 47 | 48 | // 每隔3个小时(通过setFetchPatchIntervalByHours设置)去访问后台时候有更新,通过handler实现轮训的效果 49 | TinkerPatch.with().fetchPatchUpdateAndPollWithInterval(); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/view/BaseFragment.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.view; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.os.Bundle; 6 | import android.view.Gravity; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.Toast; 11 | 12 | import androidx.annotation.Nullable; 13 | import androidx.appcompat.app.AppCompatActivity; 14 | import androidx.fragment.app.Fragment; 15 | 16 | public abstract class BaseFragment extends Fragment { 17 | 18 | 19 | protected Activity mActivity; 20 | 21 | @Override 22 | public void onAttach(Context context) { 23 | super.onAttach(context); 24 | mActivity = (AppCompatActivity) context; 25 | } 26 | 27 | @Override 28 | public void onCreate(@Nullable Bundle savedInstanceState) { 29 | super.onCreate(savedInstanceState); 30 | } 31 | 32 | @Nullable 33 | @Override 34 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 35 | return setLayoutView(inflater, container, savedInstanceState); 36 | } 37 | 38 | @Override 39 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 40 | findViewById(view); 41 | setViewData(view); 42 | setClickEvent(view); 43 | } 44 | 45 | public void showToast(String value){ 46 | Toast toast = Toast.makeText(getContext(),value,Toast.LENGTH_SHORT); 47 | toast.setGravity(Gravity.CENTER,0,0); 48 | toast.show(); 49 | } 50 | /** 51 | * 设置布局 52 | * @param inflater 53 | * @param container 54 | * @param savedInstanceState 55 | * @return 56 | */ 57 | public abstract View setLayoutView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState); 58 | 59 | /** 60 | * findViewById 61 | */ 62 | public void findViewById(View view) { 63 | } 64 | 65 | /** 66 | * setViewData 67 | */ 68 | public void setViewData(View view) { 69 | } 70 | 71 | /** 72 | * setClickEvent 73 | */ 74 | public void setClickEvent(View view) { 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/view/ChannelFragment.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.view; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.text.Editable; 6 | import android.text.TextUtils; 7 | import android.text.TextWatcher; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.EditText; 12 | import android.widget.ImageView; 13 | 14 | import com.tencent.stat.StatService; 15 | import com.z.exoplayertest.R; 16 | import com.z.exoplayertest.adpter.ChannelAdaptr; 17 | import com.z.exoplayertest.database.Channel; 18 | import com.z.exoplayertest.database.ChannelDao; 19 | 20 | import java.util.List; 21 | import java.util.Properties; 22 | 23 | import androidx.recyclerview.widget.DefaultItemAnimator; 24 | import androidx.recyclerview.widget.DividerItemDecoration; 25 | import androidx.recyclerview.widget.LinearLayoutManager; 26 | import androidx.recyclerview.widget.RecyclerView; 27 | 28 | /** 29 | * http://www.hdpfans.com/thread-834810-1-2.html 30 | * http://www.hdpfans.com/thread-820088-1-1.html 31 | * https://exoplayer.dev/hls.html 32 | */ 33 | public class ChannelFragment extends BaseFragment { 34 | 35 | private View view = null; 36 | private RecyclerView mRecyclerView; 37 | private RecyclerView.LayoutManager mLayoutManager; 38 | private EditText etQuery; 39 | private ImageView ivCancel; 40 | private ChannelAdaptr mAdapter; 41 | private List channelList; 42 | private ChannelDao channelDao; 43 | private Properties prop; 44 | private OnRefreshListener onRefreshListener; 45 | public static ChannelFragment instance = null; 46 | 47 | public ChannelFragment() { 48 | } 49 | 50 | public static ChannelFragment newInstance() { 51 | if (instance == null) { 52 | instance = new ChannelFragment(); 53 | } 54 | Bundle args = new Bundle(); 55 | instance.setArguments(args); 56 | return instance; 57 | } 58 | 59 | @Override 60 | public void onCreate(Bundle savedInstanceState) { 61 | super.onCreate(savedInstanceState); 62 | if (getArguments() != null) { 63 | 64 | } 65 | } 66 | 67 | @Override 68 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 69 | if (null != view) { 70 | ViewGroup parent = (ViewGroup) view.getParent(); 71 | if (null != parent) { 72 | parent.removeView(view); 73 | } 74 | } else { 75 | view = setLayoutView(inflater, container, savedInstanceState); 76 | initData(); 77 | initView(view); 78 | } 79 | return view; 80 | } 81 | 82 | private void initView(View view) { 83 | etQuery = view.findViewById(R.id.query); 84 | ivCancel = view.findViewById(R.id.cancel); 85 | ivCancel.setOnClickListener(new View.OnClickListener() { 86 | @Override 87 | public void onClick(View v) { 88 | etQuery.setText(""); 89 | } 90 | }); 91 | etQuery.addTextChangedListener(new TextWatcher() { 92 | @Override 93 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 94 | 95 | } 96 | 97 | @Override 98 | public void onTextChanged(CharSequence s, int start, int before, int count) { 99 | if (TextUtils.isEmpty(s.toString())){ 100 | ivCancel.setVisibility(View.INVISIBLE); 101 | ivCancel.setEnabled(false); 102 | refresh(); 103 | }else { 104 | ivCancel.setVisibility(View.VISIBLE); 105 | ivCancel.setEnabled(true); 106 | List list = channelDao.queryByName(s.toString()); 107 | channelList.clear(); 108 | channelList.addAll(list); 109 | if (mAdapter != null) { 110 | mAdapter.notifyDataSetChanged(); 111 | } 112 | } 113 | } 114 | 115 | @Override 116 | public void afterTextChanged(Editable s) { 117 | 118 | } 119 | }); 120 | mRecyclerView = view.findViewById(R.id.rv_main); 121 | mAdapter = new ChannelAdaptr(getContext(), channelList); 122 | mAdapter.setOnItemClickListener(new ChannelAdaptr.OnItemClickListener() { 123 | @Override 124 | public void onClickItem(int position, Channel channel, boolean isClickLike) { 125 | if (isClickLike) { 126 | int like; 127 | if (channel.getIsLike() == 1) { 128 | like = 0; 129 | } else { 130 | like = 1; 131 | statisticalLike(channelList.get(position).getName()); 132 | } 133 | channelList.get(position).setIsLike(like); 134 | if (channelDao != null) { 135 | channelDao.update(new String[]{String.valueOf(like), String.valueOf(channel.getId())}); 136 | } 137 | mAdapter.notifyDataSetChanged(); 138 | onRefreshListener.onRefresh(); 139 | } else { 140 | Intent intent = new Intent(getActivity(), MainActivity.class); 141 | intent.putExtra("name", channel.getName()); 142 | intent.putExtra("url", channel.getUrl()); 143 | intent.putExtra("id", channel.getId()); 144 | intent.putExtra("isLike", channel.getIsLike()); 145 | startActivityForResult(intent, 1000); 146 | statistical(channel.getName()); 147 | } 148 | } 149 | }); 150 | mLayoutManager = new LinearLayoutManager(getContext()); 151 | //设置布局管理器 152 | mRecyclerView.setLayoutManager(mLayoutManager); 153 | //设置adapter 154 | mRecyclerView.setAdapter(mAdapter); 155 | //设置Item增加、移除动画 156 | mRecyclerView.setItemAnimator(new DefaultItemAnimator()); 157 | //添加分割线 158 | // mRecyclerView.addItemDecoration(new DividerItemDecoration( 159 | // getActivity(), DividerItemDecoration.VERTICAL)); 160 | 161 | } 162 | 163 | /** 164 | * 统计用户播放 165 | */ 166 | private void statistical(String name) { 167 | // 统计按钮状态 168 | prop = new Properties(); 169 | prop.setProperty("电视台", name); 170 | StatService.trackCustomKVEvent(getContext(), "播放情况", prop); 171 | } 172 | 173 | /** 174 | * 统计用户收藏 175 | */ 176 | private void statisticalLike(String name) { 177 | // 统计按钮状态 178 | prop = new Properties(); 179 | prop.setProperty("电视台", name); 180 | StatService.trackCustomKVEvent(getContext(), "收藏情况", prop); 181 | } 182 | 183 | private void initData() { 184 | channelDao = ChannelDao.getInstance(getContext()); 185 | channelList = channelDao.getAllData(); 186 | } 187 | 188 | @Override 189 | public View setLayoutView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 190 | return inflater.inflate(R.layout.fragment_channel, container, false); 191 | } 192 | 193 | /** 194 | * 刷新 195 | */ 196 | public void refresh() { 197 | List list = channelDao.getAllData(); 198 | channelList.clear(); 199 | channelList.addAll(list); 200 | if (mAdapter != null) { 201 | mAdapter.notifyDataSetChanged(); 202 | } 203 | } 204 | 205 | interface OnRefreshListener { 206 | void onRefresh(); 207 | } 208 | 209 | public void setOnRefreshListener(OnRefreshListener onRefreshListener) { 210 | this.onRefreshListener = onRefreshListener; 211 | } 212 | } -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/view/FetchPatchHandler.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.view; 2 | 3 | import android.os.Handler; 4 | import android.os.Message; 5 | 6 | import com.tinkerpatch.sdk.TinkerPatch; 7 | 8 | /** 9 | * Created by Administrator on 2017/10/18 0018. 10 | */ 11 | 12 | public class FetchPatchHandler extends Handler { 13 | 14 | public static final long HOUR_INTERVAL = 3600 * 1000; 15 | private long checkInterval; 16 | 17 | 18 | /** 19 | * 通过handler, 达到按照时间间隔轮训的效果 20 | */ 21 | public void fetchPatchWithInterval(int hour) { 22 | //设置TinkerPatch的时间间隔 23 | TinkerPatch.with().setFetchPatchIntervalByHours(hour); 24 | checkInterval = hour * HOUR_INTERVAL; 25 | //立刻尝试去访问,检查是否有更新 26 | sendEmptyMessage(0); 27 | } 28 | 29 | 30 | @Override 31 | public void handleMessage(Message msg) { 32 | super.handleMessage(msg); 33 | //这里使用false即可 34 | TinkerPatch.with().fetchPatchUpdate(false); 35 | //每隔一段时间都去访问后台, 增加10分钟的buffer时间 36 | sendEmptyMessageDelayed(0, checkInterval + 10 * 60 * 1000); 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/view/LikeFragment.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.view; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.text.Editable; 6 | import android.text.TextUtils; 7 | import android.text.TextWatcher; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.EditText; 12 | import android.widget.ImageView; 13 | 14 | import com.z.exoplayertest.R; 15 | import com.z.exoplayertest.adpter.ChannelAdaptr; 16 | import com.z.exoplayertest.adpter.LikeAdaptr; 17 | import com.z.exoplayertest.database.Channel; 18 | import com.z.exoplayertest.database.ChannelDao; 19 | 20 | import java.util.List; 21 | 22 | import androidx.recyclerview.widget.DefaultItemAnimator; 23 | import androidx.recyclerview.widget.LinearLayoutManager; 24 | import androidx.recyclerview.widget.RecyclerView; 25 | 26 | public class LikeFragment extends BaseFragment { 27 | 28 | private View view = null; 29 | private RecyclerView mRecyclerView; 30 | private RecyclerView.LayoutManager mLayoutManager; 31 | private EditText etQuery; 32 | private ImageView ivCancel; 33 | private LikeAdaptr mAdapter; 34 | private List channelList; 35 | private ChannelDao channelDao; 36 | OnRefreshListener onRefreshListener; 37 | public static LikeFragment instance = null; 38 | 39 | public LikeFragment() { 40 | } 41 | 42 | public static LikeFragment newInstance() { 43 | if (instance==null){ 44 | instance = new LikeFragment(); 45 | } 46 | Bundle args = new Bundle(); 47 | instance.setArguments(args); 48 | return instance; 49 | } 50 | 51 | @Override 52 | public void onCreate(Bundle savedInstanceState) { 53 | super.onCreate(savedInstanceState); 54 | if (getArguments() != null) { 55 | 56 | } 57 | } 58 | 59 | @Override 60 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 61 | if (null != view) { 62 | ViewGroup parent = (ViewGroup) view.getParent(); 63 | if (null != parent) { 64 | parent.removeView(view); 65 | } 66 | } else { 67 | view = setLayoutView(inflater, container, savedInstanceState); 68 | initData(); 69 | initView(view); 70 | } 71 | return view; 72 | } 73 | 74 | private void initView(View view) { 75 | etQuery = view.findViewById(R.id.query); 76 | ivCancel = view.findViewById(R.id.cancel); 77 | ivCancel.setOnClickListener(new View.OnClickListener() { 78 | @Override 79 | public void onClick(View v) { 80 | etQuery.setText(""); 81 | } 82 | }); 83 | etQuery.addTextChangedListener(new TextWatcher() { 84 | @Override 85 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 86 | 87 | } 88 | 89 | @Override 90 | public void onTextChanged(CharSequence s, int start, int before, int count) { 91 | if (TextUtils.isEmpty(s.toString())){ 92 | ivCancel.setVisibility(View.INVISIBLE); 93 | ivCancel.setEnabled(false); 94 | refresh(); 95 | }else { 96 | ivCancel.setVisibility(View.VISIBLE); 97 | ivCancel.setEnabled(true); 98 | List list = channelDao.queryByNameAndLike(s.toString()); 99 | channelList.clear(); 100 | channelList.addAll(list); 101 | if (mAdapter != null) { 102 | mAdapter.notifyDataSetChanged(); 103 | } 104 | } 105 | } 106 | 107 | @Override 108 | public void afterTextChanged(Editable s) { 109 | 110 | } 111 | }); 112 | mRecyclerView = view.findViewById(R.id.rv_main); 113 | mAdapter = new LikeAdaptr(getContext(), channelList); 114 | mAdapter.setOnItemClickListener(new LikeAdaptr.OnItemClickListener() { 115 | @Override 116 | public void onClickItem(int position, Channel channel, boolean isClickLike) { 117 | if (isClickLike) { 118 | if (channelDao!=null){ 119 | channelDao.update(new String[]{"0",String.valueOf(channel.getId())}); 120 | } 121 | channelList.remove(position); 122 | mAdapter.notifyDataSetChanged(); 123 | onRefreshListener.onRefresh(); 124 | } else { 125 | Intent intent = new Intent(getActivity(), MainActivity.class); 126 | intent.putExtra("name", channel.getName()); 127 | intent.putExtra("url", channel.getUrl()); 128 | intent.putExtra("id", channel.getId()); 129 | intent.putExtra("isLike", channel.getIsLike()); 130 | startActivityForResult(intent,1000); 131 | } 132 | } 133 | }); 134 | mLayoutManager = new LinearLayoutManager(getContext()); 135 | //设置布局管理器 136 | mRecyclerView.setLayoutManager(mLayoutManager); 137 | //设置adapter 138 | mRecyclerView.setAdapter(mAdapter); 139 | //设置Item增加、移除动画 140 | mRecyclerView.setItemAnimator(new DefaultItemAnimator()); 141 | //添加分割线 142 | // mRecyclerView.addItemDecoration(new DividerItemDecoration( 143 | // getActivity(), DividerItemDecoration.VERTICAL)); 144 | 145 | } 146 | 147 | private void initData() { 148 | channelDao = ChannelDao.getInstance(getContext()); 149 | channelList = channelDao.queryUserLike(); 150 | } 151 | 152 | @Override 153 | public View setLayoutView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 154 | return inflater.inflate(R.layout.fragment_channel, container, false); 155 | } 156 | 157 | public void refresh(){ 158 | List list = channelDao.queryUserLike(); 159 | channelList.clear(); 160 | channelList.addAll(list); 161 | if (mAdapter!=null){ 162 | mAdapter.notifyDataSetChanged(); 163 | } 164 | } 165 | 166 | interface OnRefreshListener { 167 | void onRefresh(); 168 | } 169 | 170 | public void setOnRefreshListener(OnRefreshListener onRefreshListener) { 171 | this.onRefreshListener = onRefreshListener; 172 | } 173 | } -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/view/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.view; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.content.Intent; 6 | import android.content.pm.ActivityInfo; 7 | import android.content.res.Configuration; 8 | import android.net.Uri; 9 | import android.os.Build; 10 | import android.os.Bundle; 11 | import android.os.PowerManager; 12 | import android.view.Display; 13 | import android.view.MenuItem; 14 | import android.view.View; 15 | import android.view.ViewGroup; 16 | import android.view.WindowManager; 17 | import android.widget.ImageView; 18 | import android.widget.TextView; 19 | import android.widget.Toast; 20 | 21 | import com.google.android.exoplayer2.DefaultRenderersFactory; 22 | import com.google.android.exoplayer2.ExoPlaybackException; 23 | import com.google.android.exoplayer2.ExoPlayer; 24 | import com.google.android.exoplayer2.ExoPlayerFactory; 25 | import com.google.android.exoplayer2.PlaybackParameters; 26 | import com.google.android.exoplayer2.PlaybackPreparer; 27 | import com.google.android.exoplayer2.RenderersFactory; 28 | import com.google.android.exoplayer2.SimpleExoPlayer; 29 | import com.google.android.exoplayer2.ext.rtmp.RtmpDataSourceFactory; 30 | import com.google.android.exoplayer2.source.ExtractorMediaSource; 31 | import com.google.android.exoplayer2.source.MediaSource; 32 | import com.google.android.exoplayer2.source.TrackGroupArray; 33 | import com.google.android.exoplayer2.source.hls.DefaultHlsDataSourceFactory; 34 | import com.google.android.exoplayer2.source.hls.HlsMediaSource; 35 | import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; 36 | import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; 37 | import com.google.android.exoplayer2.trackselection.TrackSelection; 38 | import com.google.android.exoplayer2.trackselection.TrackSelectionArray; 39 | import com.google.android.exoplayer2.ui.PlayerView; 40 | import com.google.android.exoplayer2.upstream.DataSource; 41 | import com.google.android.exoplayer2.upstream.DataSpec; 42 | import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; 43 | import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; 44 | import com.google.android.exoplayer2.upstream.HttpDataSource; 45 | import com.google.android.exoplayer2.util.EventLogger; 46 | import com.google.android.exoplayer2.util.Util; 47 | import com.tencent.stat.StatService; 48 | import com.z.exoplayertest.BuildConfig; 49 | import com.z.exoplayertest.R; 50 | import com.z.exoplayertest.database.ChannelDao; 51 | 52 | import java.io.IOException; 53 | import java.util.Properties; 54 | 55 | import androidx.appcompat.app.AppCompatActivity; 56 | 57 | import static com.google.android.exoplayer2.ui.PlayerView.SHOW_BUFFERING_ALWAYS; 58 | 59 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { 60 | 61 | private DefaultTrackSelector trackSelector; 62 | private PlayerView playerView; 63 | private SimpleExoPlayer player; 64 | private Activity activity; 65 | private String url; 66 | private String tvName; 67 | private int isLike; 68 | private int id; 69 | /** 70 | * 保持屏幕常亮 71 | */ 72 | private PowerManager.WakeLock mWakeLock; 73 | private ImageView ivFull; 74 | /** 75 | * 自适应高度 76 | */ 77 | private boolean isWrapContent = false; 78 | private ImageView ivLike; 79 | private TextView tvError; 80 | private int like; 81 | private int width; 82 | private Properties prop; 83 | 84 | @SuppressLint("InvalidWakeLockTag") 85 | @Override 86 | protected void onCreate(Bundle savedInstanceState) { 87 | activity = this; 88 | super.onCreate(savedInstanceState); 89 | setContentView(R.layout.activity_main); 90 | setHalfTransparent(); 91 | setFitSystemWindow(false); 92 | PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); 93 | if (powerManager != null) { 94 | mWakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "WakeLock"); 95 | } 96 | 97 | Intent intent = getIntent(); 98 | url = intent.getStringExtra("url"); 99 | tvName = intent.getStringExtra("name"); 100 | isLike = intent.getIntExtra("isLike", 0); 101 | id = intent.getIntExtra("id", 1); 102 | playerView = findViewById(R.id.player_view); 103 | tvError = findViewById(R.id.play_error); 104 | ivLike = findViewById(R.id.like); 105 | like = isLike; 106 | if (isLike == 1) { 107 | ivLike.setImageDrawable(getDrawable(R.mipmap.like)); 108 | } else { 109 | ivLike.setImageDrawable(getDrawable(R.mipmap.unlike)); 110 | } 111 | ivLike.setOnClickListener(this); 112 | Display display = getWindowManager().getDefaultDisplay(); 113 | width = display.getWidth(); 114 | playerView.getLayoutParams().height = (width / 16) * 9; 115 | TextView name = findViewById(R.id.tv_name); 116 | name.setText(tvName); 117 | playerView.setShowBuffering(SHOW_BUFFERING_ALWAYS); 118 | ivFull = findViewById(R.id.full); 119 | ivFull.setOnClickListener(this); 120 | findViewById(R.id.back).setOnClickListener(this); 121 | if (player != null && player.isPlaying()) { 122 | player.stop(true); 123 | player.release(); 124 | player = null; 125 | } 126 | playerView.getVideoSurfaceView().setKeepScreenOn(true); 127 | try { 128 | TrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(); 129 | RenderersFactory renderersFactory = buildRenderersFactory(false); 130 | trackSelector = new DefaultTrackSelector(trackSelectionFactory); 131 | trackSelector.setParameters(new DefaultTrackSelector.ParametersBuilder().build()); 132 | Uri uri = Uri.parse(url); 133 | player = ExoPlayerFactory.newSimpleInstance(activity, renderersFactory, trackSelector); 134 | player.setPlayWhenReady(true); 135 | player.addAnalyticsListener(new EventLogger(trackSelector)); 136 | playerView.setPlayer(player); 137 | playerView.setPlaybackPreparer(new PlaybackPreparer() { 138 | @Override 139 | public void preparePlayback() { 140 | if (tvError.getVisibility() == View.VISIBLE) { 141 | tvError.setVisibility(View.GONE); 142 | } 143 | player.retry(); 144 | } 145 | }); 146 | player.addListener(eventListener); 147 | if (url.contains("rtmp")) { 148 | //rtmp://58.200.131.2:1935/livetv/cctv1hd 149 | DataSource.Factory rtmpDataSourceFactory = new RtmpDataSourceFactory(); 150 | MediaSource rtmpMediaSource = new ExtractorMediaSource.Factory(rtmpDataSourceFactory).createMediaSource(uri); 151 | player.prepare(rtmpMediaSource, true, false); 152 | }else if (url.contains("m3u8")){ 153 | //hls 154 | // https://cdn.letv-cdn.com/2018/12/05/JOCeEEUuoteFrjCg/playlist.m3u8 155 | //http://ivi.bupt.edu.cn/hls/cctv1hd.m3u8 156 | DataSource.Factory hlsDataSourceFactory = 157 | new DefaultHttpDataSourceFactory(Util.getUserAgent(this, "MainActivity")); 158 | HlsMediaSource hlsMediaSource = 159 | new HlsMediaSource.Factory(hlsDataSourceFactory).createMediaSource(uri); 160 | player.prepare(hlsMediaSource, true, false); 161 | }else { 162 | //普通网络地址 163 | //https://media.w3.org/2010/05/sintel/trailer.mp4 164 | DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this, Util.getUserAgent(this, "MainActivity")); 165 | MediaSource mediaSource = new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri); 166 | player.prepare(mediaSource, true, false); 167 | } 168 | 169 | } catch (Exception e) { 170 | e.printStackTrace(); 171 | } 172 | } 173 | 174 | private ExoPlayer.EventListener eventListener = new ExoPlayer.EventListener() { 175 | @Override 176 | public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { 177 | } 178 | 179 | @Override 180 | public void onLoadingChanged(boolean isLoading) { 181 | } 182 | 183 | @Override 184 | public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { 185 | switch (playbackState) { 186 | case ExoPlayer.STATE_ENDED: 187 | //Stop playback and return to start position 188 | break; 189 | case ExoPlayer.STATE_READY: 190 | if (tvError.getVisibility() == View.VISIBLE) { 191 | tvError.setVisibility(View.GONE); 192 | } 193 | if (!isWrapContent) { 194 | //设置自适应高度 195 | playerView.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; 196 | playerView.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; 197 | isWrapContent = true; 198 | } 199 | break; 200 | case ExoPlayer.STATE_BUFFERING: 201 | break; 202 | case ExoPlayer.STATE_IDLE: 203 | break; 204 | default: 205 | } 206 | } 207 | 208 | @Override 209 | public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { 210 | } 211 | 212 | @Override 213 | public void onPlayerError(ExoPlaybackException error) { 214 | if (error.type == ExoPlaybackException.TYPE_SOURCE) { 215 | tvError.setVisibility(View.VISIBLE); 216 | IOException cause = error.getSourceException(); 217 | if (cause instanceof HttpDataSource.HttpDataSourceException) { 218 | // An HTTP error occurred. 219 | HttpDataSource.HttpDataSourceException httpError = (HttpDataSource.HttpDataSourceException) cause; 220 | // This is the request for which the error occurred. 221 | DataSpec requestDataSpec = httpError.dataSpec; 222 | // It's possible to find out more about the error both by casting and by 223 | // querying the cause. 224 | if (httpError instanceof HttpDataSource.InvalidResponseCodeException) { 225 | // Cast to InvalidResponseCodeException and retrieve the response code, 226 | // message and headers. 227 | } else { 228 | // Try calling httpError.getCause() to retrieve the underlying cause, 229 | // although note that it may be null. 230 | } 231 | } 232 | } 233 | } 234 | }; 235 | 236 | @Override 237 | protected void onDestroy() { 238 | super.onDestroy(); 239 | if (player != null) { 240 | player.stop(true); 241 | player.release(); 242 | player = null; 243 | } 244 | } 245 | 246 | @Override 247 | public void onBackPressed() { 248 | setResult(like == isLike ? 100 : 200); 249 | super.onBackPressed(); 250 | } 251 | 252 | @Override 253 | protected void onPause() { 254 | super.onPause(); 255 | if (player != null) { 256 | player.stop(false); 257 | } 258 | if (mWakeLock != null) { 259 | mWakeLock.release(); 260 | } 261 | } 262 | 263 | @Override 264 | protected void onResume() { 265 | super.onResume(); 266 | if (mWakeLock != null) { 267 | mWakeLock.acquire(); 268 | } 269 | } 270 | 271 | /** 272 | * Returns whether extension renderers should be used. 273 | */ 274 | public boolean useExtensionRenderers() { 275 | return "withExtensions".equals(BuildConfig.FLAVOR); 276 | } 277 | 278 | public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) { 279 | @DefaultRenderersFactory.ExtensionRendererMode 280 | int extensionRendererMode = 281 | useExtensionRenderers() 282 | ? (preferExtensionRenderer 283 | ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER 284 | : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) 285 | : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; 286 | return new DefaultRenderersFactory(this) 287 | .setExtensionRendererMode(extensionRendererMode); 288 | } 289 | 290 | /** 291 | * 半透明状态栏 292 | */ 293 | protected void setHalfTransparent() { 294 | 295 | if (Build.VERSION.SDK_INT >= 21) { 296 | //21表示5.0 297 | View decorView = getWindow().getDecorView(); 298 | int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; 299 | decorView.setSystemUiVisibility(option); 300 | getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 301 | 302 | } else if (Build.VERSION.SDK_INT >= 19) { 303 | //19表示4.4 304 | getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 305 | //虚拟键盘也透明 306 | // getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); 307 | } 308 | } 309 | 310 | /** 311 | * 如果需要内容紧贴着StatusBar 312 | * 应该在对应的xml布局文件中,设置根布局fitsSystemWindows=true。 313 | */ 314 | private View contentViewGroup; 315 | 316 | protected void setFitSystemWindow(boolean fitSystemWindow) { 317 | if (contentViewGroup == null) { 318 | contentViewGroup = ((ViewGroup) findViewById(android.R.id.content)).getChildAt(0); 319 | } 320 | contentViewGroup.setFitsSystemWindows(fitSystemWindow); 321 | } 322 | 323 | @Override 324 | public void onWindowFocusChanged(boolean hasFocus) { 325 | super.onWindowFocusChanged(hasFocus); 326 | if (hasFocus && Build.VERSION.SDK_INT >= 19) { 327 | View decorView = getWindow().getDecorView(); 328 | decorView.setSystemUiVisibility( 329 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 330 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 331 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 332 | | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 333 | | View.SYSTEM_UI_FLAG_FULLSCREEN 334 | | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); 335 | } 336 | } 337 | 338 | @Override 339 | public void onClick(View v) { 340 | int id = v.getId(); 341 | if (id == R.id.like) { 342 | if (like == 1) { 343 | like = 0; 344 | ivLike.setImageDrawable(getDrawable(R.mipmap.unlike)); 345 | } else { 346 | like = 1; 347 | ivLike.setImageDrawable(getDrawable(R.mipmap.like)); 348 | statisticalLike(tvName); 349 | } 350 | ChannelDao.getInstance(this).update(new String[]{String.valueOf(like), String.valueOf(this.id)}); 351 | } else if (id == R.id.full) { 352 | //获取设置的配置信息 353 | Configuration mConfiguration = getResources().getConfiguration(); 354 | //获取屏幕方向 355 | int ori = mConfiguration.orientation; 356 | if (ori == Configuration.ORIENTATION_PORTRAIT) { 357 | //竖屏 强制为横屏 358 | if (!isWrapContent) { 359 | playerView.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; 360 | playerView.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; 361 | isWrapContent = true; 362 | } 363 | ivFull.setVisibility(View.GONE); 364 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 365 | } 366 | } else if (id == R.id.back) { 367 | //获取设置的配置信息 368 | Configuration mConfiguration = getResources().getConfiguration(); 369 | //获取屏幕方向 370 | int ori = mConfiguration.orientation; 371 | if (ori == Configuration.ORIENTATION_LANDSCAPE) { 372 | //横屏 强制为竖屏 373 | ivFull.setVisibility(View.VISIBLE); 374 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 375 | if (!player.isPlaying()){ 376 | playerView.getLayoutParams().height = (width / 16) * 9; 377 | isWrapContent = false; 378 | } 379 | } else if (ori == Configuration.ORIENTATION_PORTRAIT) { 380 | //竖屏 退出 381 | if (player != null) { 382 | player.stop(); 383 | player.release(); 384 | } 385 | setResult(like == isLike ? 100 : 200); 386 | finish(); 387 | } 388 | } 389 | } 390 | /** 391 | * 统计用户收藏 392 | */ 393 | private void statisticalLike(String name) { 394 | // 统计按钮状态 395 | prop = new Properties(); 396 | prop.setProperty("电视台", name); 397 | StatService.trackCustomKVEvent(this, "收藏情况", prop); 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/view/SelectTvActivity.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.view; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.content.Intent; 6 | import android.content.pm.PackageManager; 7 | import android.graphics.Color; 8 | import android.graphics.Typeface; 9 | import android.os.Build; 10 | import android.os.Bundle; 11 | import android.text.TextPaint; 12 | import android.view.Display; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | import android.view.Window; 16 | import android.view.WindowManager; 17 | import android.widget.TextView; 18 | 19 | import com.google.android.material.tabs.TabLayout; 20 | import com.z.exoplayertest.adpter.PagerAdapter; 21 | import com.z.exoplayertest.R; 22 | 23 | import androidx.annotation.Nullable; 24 | import androidx.appcompat.app.AppCompatActivity; 25 | import androidx.appcompat.widget.AppCompatTextView; 26 | import androidx.core.app.ActivityCompat; 27 | import androidx.viewpager.widget.ViewPager; 28 | 29 | public class SelectTvActivity extends AppCompatActivity{ 30 | private TabLayout tablayout; 31 | private ViewPager viewpager; 32 | private String[] titles = {"频道", "收藏", "设置"}; 33 | private int textMinWidth = 0; 34 | private int textMaxWidth = 0; 35 | private boolean isClickTab; 36 | private float mLastPositionOffsetSum; 37 | 38 | @Override 39 | protected void onCreate(@Nullable Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | setContentView(R.layout.activity_select_tv); 42 | setStatusBarFullTransparent(); 43 | setAndroidNativeLightStatusBar(this,true); 44 | setFitSystemWindow(false); 45 | tablayout = findViewById(R.id.tablayout); 46 | viewpager = findViewById(R.id.viewpager); 47 | viewpager.setAdapter(new PagerAdapter(this, getSupportFragmentManager())); 48 | tablayout.setupWithViewPager(viewpager); 49 | checkPermission(); 50 | initSize(); 51 | for (int i = 0; i < titles.length; i++) { 52 | TabLayout.Tab tab = tablayout.getTabAt(i); 53 | assert tab != null; 54 | //给tab自定义样式 55 | tab.setCustomView(R.layout.tab_item); 56 | assert tab.getCustomView() != null; 57 | AppCompatTextView textView = tab.getCustomView().findViewById(R.id.tab_text); 58 | textView.setText(titles[i]); 59 | AppCompatTextView compatTextView = ((AppCompatTextView) tab.getCustomView().findViewById(R.id.tab_text)); 60 | if (i == 0) { 61 | //第一个tab被选中 62 | compatTextView.setSelected(true); 63 | compatTextView.setWidth(textMaxWidth); 64 | compatTextView.setTypeface(Typeface.DEFAULT_BOLD); 65 | // ((WaveView) tab.getCustomView().findViewById(R.id.wave)).setWaveWidth(textMaxWidth, true); 66 | } else { 67 | compatTextView.setWidth(textMinWidth); 68 | // ((WaveView) tab.getCustomView().findViewById(R.id.wave)).setWaveWidth(textMinWidth, false); 69 | } 70 | } 71 | 72 | viewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { 73 | @Override 74 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 75 | // 当前总的偏移量 76 | float currentPositionOffsetSum = position + positionOffset; 77 | // 上次滑动的总偏移量大于此次滑动的总偏移量,页面从右向左进入(手指从右向左滑动) 78 | boolean rightToLeft = mLastPositionOffsetSum <= currentPositionOffsetSum; 79 | if (currentPositionOffsetSum == mLastPositionOffsetSum) { 80 | return; 81 | } 82 | int enterPosition; 83 | int leavePosition; 84 | float percent; 85 | if (rightToLeft) { 86 | // 从右向左滑 87 | enterPosition = (positionOffset == 0.0f) ? position : position + 1; 88 | leavePosition = enterPosition - 1; 89 | percent = (positionOffset == 0.0f) ? 1.0f : positionOffset; 90 | } else { 91 | // 从左向右滑 92 | enterPosition = position; 93 | leavePosition = position + 1; 94 | percent = 1 - positionOffset; 95 | } 96 | if (!isClickTab) { 97 | int width = (int) (textMinWidth + (textMaxWidth - textMinWidth) * (1 - percent)); 98 | ((AppCompatTextView) (tablayout.getTabAt(leavePosition).getCustomView().findViewById(R.id.tab_text))) 99 | .setWidth(width); 100 | ((AppCompatTextView) (tablayout.getTabAt(enterPosition).getCustomView().findViewById(R.id.tab_text))) 101 | .setWidth((int) (textMinWidth + (textMaxWidth - textMinWidth) * percent)); 102 | } 103 | 104 | mLastPositionOffsetSum = currentPositionOffsetSum; 105 | } 106 | 107 | @Override 108 | public void onPageSelected(int position) { 109 | for (int i = 0; i < 3; i++) { 110 | TabLayout.Tab tab = tablayout.getTabAt(i); 111 | assert tab != null; 112 | if (i == position) { 113 | ((AppCompatTextView) (tab.getCustomView().findViewById(R.id.tab_text))).setTypeface(Typeface.DEFAULT_BOLD); 114 | // ((WaveView) tab.getCustomView().findViewById(R.id.wave)).setWaveWidth(textMaxWidth, true); 115 | } else { 116 | ((AppCompatTextView) (tab.getCustomView().findViewById(R.id.tab_text))).setTypeface(Typeface.DEFAULT); 117 | // ((WaveView) tab.getCustomView().findViewById(R.id.wave)).setWaveWidth(textMinWidth, false); 118 | } 119 | } 120 | 121 | } 122 | 123 | @Override 124 | public void onPageScrollStateChanged(int state) { 125 | if (state == 0) { 126 | isClickTab = false; 127 | } 128 | } 129 | }); 130 | 131 | tablayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { 132 | @Override 133 | public void onTabSelected(TabLayout.Tab tab) { 134 | isClickTab = true; 135 | tab.getCustomView().findViewById(R.id.tab_text).setSelected(true); 136 | viewpager.setCurrentItem(tab.getPosition()); 137 | ((AppCompatTextView) (tab.getCustomView().findViewById(R.id.tab_text))).setWidth(textMaxWidth); 138 | // ((WaveView) tab.getCustomView().findViewById(R.id.wave)).setWaveWidth(textMaxWidth, true); 139 | } 140 | 141 | @Override 142 | public void onTabUnselected(TabLayout.Tab tab) { 143 | tab.getCustomView().findViewById(R.id.tab_text).setSelected(false); 144 | ((AppCompatTextView) (tab.getCustomView().findViewById(R.id.tab_text))).setWidth(textMinWidth); 145 | // ((WaveView) tab.getCustomView().findViewById(R.id.wave)).setWaveWidth(textMinWidth, false); 146 | } 147 | 148 | @Override 149 | public void onTabReselected(TabLayout.Tab tab) { 150 | 151 | 152 | } 153 | }); 154 | viewpager.setCurrentItem(0); 155 | viewpager.setOffscreenPageLimit(2); 156 | ChannelFragment.newInstance().setOnRefreshListener(onRefreshLikeListener); 157 | LikeFragment.newInstance().setOnRefreshListener(onRefreshChannelListener); 158 | SettingFragment.newInstance().setOnRefreshListener(onRefreshListener); 159 | } 160 | 161 | private void initSize() { 162 | TextView tv = new TextView(this); 163 | tv.setTextSize(getResources().getDimension(R.dimen.title_no_selected)); 164 | TextPaint textPaint = tv.getPaint(); 165 | textMinWidth = (int) textPaint.measureText("频道"); 166 | tv = new TextView(this); 167 | tv.setTextSize(getResources().getDimension(R.dimen.title_selected)); 168 | textPaint = tv.getPaint(); 169 | textMaxWidth = (int) textPaint.measureText("频道"); 170 | } 171 | 172 | public void checkPermission() { 173 | boolean isGranted = true; 174 | if (android.os.Build.VERSION.SDK_INT >= 23) { 175 | if (this.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 176 | //如果没有写sd卡权限 177 | isGranted = false; 178 | } 179 | if (this.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 180 | isGranted = false; 181 | } 182 | if (!isGranted) { 183 | ActivityCompat.requestPermissions(this, 184 | new String[]{Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission 185 | .ACCESS_FINE_LOCATION, 186 | Manifest.permission.READ_EXTERNAL_STORAGE, 187 | Manifest.permission.WRITE_EXTERNAL_STORAGE}, 188 | 102); 189 | } 190 | } 191 | } 192 | 193 | /** 194 | * 全透状态栏 195 | */ 196 | protected void setStatusBarFullTransparent() { 197 | if (Build.VERSION.SDK_INT >= 21) { 198 | //21表示5.0 199 | Window window = getWindow(); 200 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 201 | window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 202 | | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 203 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 204 | window.setStatusBarColor(Color.TRANSPARENT); 205 | } else if (Build.VERSION.SDK_INT >= 19) { 206 | //19表示4.4 207 | getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 208 | //虚拟键盘也透明 209 | //getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); 210 | } 211 | } 212 | 213 | private static void setAndroidNativeLightStatusBar(Activity activity, boolean dark) { 214 | View decor = activity.getWindow().getDecorView(); 215 | if (dark) { 216 | decor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); 217 | } else { 218 | decor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 219 | } 220 | } 221 | 222 | /** 223 | * 如果需要内容紧贴着StatusBar 224 | * 应该在对应的xml布局文件中,设置根布局fitsSystemWindows=true。 225 | */ 226 | private View contentViewGroup; 227 | 228 | protected void setFitSystemWindow(boolean fitSystemWindow) { 229 | if (contentViewGroup == null) { 230 | contentViewGroup = ((ViewGroup) findViewById(android.R.id.content)).getChildAt(0); 231 | } 232 | contentViewGroup.setFitsSystemWindows(fitSystemWindow); 233 | } 234 | 235 | /** 236 | * 刷新收藏页面 237 | */ 238 | ChannelFragment.OnRefreshListener onRefreshLikeListener = new ChannelFragment.OnRefreshListener() { 239 | @Override 240 | public void onRefresh() { 241 | LikeFragment.newInstance().refresh(); 242 | } 243 | }; 244 | 245 | LikeFragment.OnRefreshListener onRefreshChannelListener = new LikeFragment.OnRefreshListener() { 246 | @Override 247 | public void onRefresh() { 248 | ChannelFragment.newInstance().refresh(); 249 | } 250 | }; 251 | 252 | SettingFragment.OnRefreshListener onRefreshListener = new SettingFragment.OnRefreshListener() { 253 | @Override 254 | public void onRefresh() { 255 | LikeFragment.newInstance().refresh(); 256 | ChannelFragment.newInstance().refresh(); 257 | } 258 | }; 259 | 260 | @Override 261 | protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { 262 | super.onActivityResult(requestCode, resultCode, data); 263 | if (resultCode==200){ 264 | ChannelFragment.newInstance().refresh(); 265 | LikeFragment.newInstance().refresh(); 266 | } 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/view/SettingFragment.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.view; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.DialogInterface; 6 | import android.content.pm.PackageInfo; 7 | import android.content.pm.PackageManager; 8 | import android.os.Bundle; 9 | import android.os.Environment; 10 | import android.text.TextUtils; 11 | import android.view.LayoutInflater; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.view.inputmethod.InputMethodManager; 15 | import android.widget.EditText; 16 | import android.widget.LinearLayout; 17 | import android.widget.TextView; 18 | 19 | import com.tencent.bugly.Bugly; 20 | import com.tencent.bugly.beta.Beta; 21 | import com.tencent.bugly.crashreport.CrashReport; 22 | import com.tencent.stat.StatService; 23 | import com.z.exoplayertest.R; 24 | import com.z.exoplayertest.database.Channel; 25 | import com.z.exoplayertest.database.ChannelDao; 26 | import com.z.exoplayertest.utils.FileUtil; 27 | 28 | import java.io.File; 29 | import java.util.Properties; 30 | 31 | import androidx.appcompat.app.AlertDialog; 32 | 33 | public class SettingFragment extends BaseFragment implements View.OnClickListener { 34 | 35 | private View view = null; 36 | private LinearLayout llCreateChannel; 37 | private LinearLayout llReset; 38 | private LinearLayout llCheckUpdate; 39 | private LinearLayout llFeedback; 40 | private AlertDialog.Builder builder; 41 | private ChannelDao channelDao; 42 | private Properties prop; 43 | private OnRefreshListener onRefreshListener; 44 | public static SettingFragment instance = null; 45 | 46 | public SettingFragment() { 47 | } 48 | 49 | public static SettingFragment newInstance() { 50 | if (instance == null) { 51 | instance = new SettingFragment(); 52 | } 53 | Bundle args = new Bundle(); 54 | instance.setArguments(args); 55 | return instance; 56 | } 57 | 58 | @Override 59 | public void onCreate(Bundle savedInstanceState) { 60 | super.onCreate(savedInstanceState); 61 | if (getArguments() != null) { 62 | 63 | } 64 | } 65 | 66 | @Override 67 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 68 | if (null != view) { 69 | ViewGroup parent = (ViewGroup) view.getParent(); 70 | if (null != parent) { 71 | parent.removeView(view); 72 | } 73 | } else { 74 | view = setLayoutView(inflater, container, savedInstanceState); 75 | initView(view); 76 | } 77 | return view; 78 | } 79 | 80 | private void initView(View view) { 81 | channelDao = ChannelDao.getInstance(getContext()); 82 | llCreateChannel = view.findViewById(R.id.create_channel); 83 | llCreateChannel.setOnClickListener(this); 84 | llReset = view.findViewById(R.id.reset_channel); 85 | llReset.setOnClickListener(this); 86 | llCheckUpdate = view.findViewById(R.id.check_update); 87 | llCheckUpdate.setOnClickListener(this); 88 | llFeedback = view.findViewById(R.id.feedback); 89 | llFeedback.setOnClickListener(this); 90 | TextView tvAppName = view.findViewById(R.id.app_name); 91 | tvAppName.setText(getString(R.string.app_name) + " " + versionName(getContext())); 92 | } 93 | 94 | @Override 95 | public View setLayoutView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 96 | return inflater.inflate(R.layout.fragment_setting, container, false); 97 | } 98 | 99 | @Override 100 | public void onClick(View v) { 101 | int id = v.getId(); 102 | if (id == R.id.create_channel) { 103 | showAddChannelDialog(); 104 | } else if (id == R.id.reset_channel) { 105 | //重新读取文件中的数据 106 | showResetDialog(); 107 | } else if (id == R.id.check_update) { 108 | Beta.checkUpgrade(); 109 | } else if (id == R.id.feedback) { 110 | // CrashReport.testJavaCrash(); 111 | showFeedbackDialog(); 112 | } 113 | } 114 | 115 | public String versionName(Context context) { 116 | PackageManager manager = context.getPackageManager(); 117 | String name = null; 118 | try { 119 | PackageInfo info = manager.getPackageInfo(context.getPackageName(), 0); 120 | name = info.versionName; 121 | } catch (PackageManager.NameNotFoundException e) { 122 | e.printStackTrace(); 123 | } 124 | return name; 125 | } 126 | 127 | /** 128 | * 添加频道弹窗 129 | */ 130 | private void showAddChannelDialog() { 131 | View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_edit, null); 132 | final EditText etName = view.findViewById(R.id.name); 133 | final EditText etUrl = view.findViewById(R.id.url); 134 | 135 | final AlertDialog alertDialog = new AlertDialog.Builder(getActivity()).setPositiveButton("添加", null).create(); 136 | alertDialog.setTitle("添加频道"); 137 | alertDialog.setIcon(R.mipmap.ic_launcher); 138 | alertDialog.setView(view); 139 | alertDialog.setOnShowListener(new DialogInterface.OnShowListener() { 140 | @Override 141 | public void onShow(DialogInterface dialog) { 142 | //为了避免点击 positive 按钮后直接关闭 dialog,把点击事件拿出来设置 143 | alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) 144 | .setOnClickListener(new View.OnClickListener() { 145 | @Override 146 | public void onClick(View v) { 147 | String name = etName.getText().toString(); 148 | String url = etUrl.getText().toString(); 149 | if (TextUtils.isEmpty(name) || TextUtils.isEmpty(url)) { 150 | showToast("名称和链接不能为空"); 151 | return; 152 | } 153 | Channel channel = new Channel(name, url, 1); 154 | channelDao.add(channel); 155 | FileUtil.writeFile(FileUtil.USER_CHANNEL_FILE_PATH, name + "&&" + url); 156 | onRefreshListener.onRefresh(); 157 | showToast("频道添加成功"); 158 | alertDialog.dismiss(); 159 | InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); 160 | inputMethodManager.hideSoftInputFromWindow(etUrl.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); 161 | statisticalAdd(name + "&&" + url); 162 | } 163 | }); 164 | } 165 | }); 166 | alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "取消", new DialogInterface.OnClickListener() { 167 | @Override 168 | public void onClick(DialogInterface dialog, int which) { 169 | InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); 170 | inputMethodManager.hideSoftInputFromWindow(etUrl.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); 171 | dialog.dismiss(); 172 | } 173 | }); 174 | alertDialog.show(); 175 | alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(getContext().getResources().getColor(R.color.black)); 176 | alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setTextColor(getContext().getResources().getColor(R.color.black)); 177 | alertDialog.setCancelable(false); 178 | } 179 | 180 | /** 181 | * 反馈弹窗 182 | */ 183 | private void showFeedbackDialog() { 184 | View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_feedback, null); 185 | final EditText editText = view.findViewById(R.id.dialog_feedback); 186 | final AlertDialog alertDialog = new AlertDialog.Builder(getActivity()).setPositiveButton("提交", null).create(); 187 | alertDialog.setTitle("反馈与建议"); 188 | alertDialog.setIcon(R.mipmap.ic_launcher); 189 | alertDialog.setView(view); 190 | alertDialog.setOnShowListener(new DialogInterface.OnShowListener() { 191 | @Override 192 | public void onShow(final DialogInterface dialog) { 193 | //为了避免点击 positive 按钮后直接关闭 dialog,把点击事件拿出来设置 194 | alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) 195 | .setOnClickListener(new View.OnClickListener() { 196 | @Override 197 | public void onClick(View v) { 198 | String str = editText.getText().toString(); 199 | if (str.length() < 5) { 200 | showToast("请输入五个字以上"); 201 | return; 202 | } 203 | InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); 204 | inputMethodManager.hideSoftInputFromWindow(editText.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); 205 | statisticalFeedBack(str); 206 | alertDialog.dismiss(); 207 | showToast("感谢您的宝贵意见"); 208 | } 209 | }); 210 | } 211 | }); 212 | alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "取消", new DialogInterface.OnClickListener() { 213 | @Override 214 | public void onClick(DialogInterface dialog, int which) { 215 | InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); 216 | inputMethodManager.hideSoftInputFromWindow(editText.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); 217 | dialog.dismiss(); 218 | } 219 | }); 220 | alertDialog.show(); 221 | alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(getContext().getResources().getColor(R.color.black)); 222 | alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setTextColor(getContext().getResources().getColor(R.color.black)); 223 | alertDialog.setCancelable(false); 224 | } 225 | 226 | /** 227 | * 重置 228 | */ 229 | private void showResetDialog() { 230 | final AlertDialog alertDialog = new AlertDialog.Builder(getActivity()).setPositiveButton("继续", null).create(); 231 | alertDialog.setTitle("更新"); 232 | alertDialog.setMessage("是否更新频道列表?"); 233 | alertDialog.setIcon(R.mipmap.ic_launcher); 234 | alertDialog.setOnShowListener(new DialogInterface.OnShowListener() { 235 | @Override 236 | public void onShow(final DialogInterface dialog) { 237 | //为了避免点击 positive 按钮后直接关闭 dialog,把点击事件拿出来设置 238 | alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) 239 | .setOnClickListener(new View.OnClickListener() { 240 | @Override 241 | public void onClick(View v) { 242 | channelDao.deleteAll(); 243 | channelDao.addList(FileUtil.getChannelFromTxt(getContext())); 244 | onRefreshListener.onRefresh(); 245 | showToast("更新成功"); 246 | dialog.dismiss(); 247 | } 248 | }); 249 | } 250 | }); 251 | alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "取消", new DialogInterface.OnClickListener() { 252 | @Override 253 | public void onClick(DialogInterface dialog, int which) { 254 | dialog.dismiss(); 255 | } 256 | }); 257 | alertDialog.show(); 258 | alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(getContext().getResources().getColor(R.color.black)); 259 | alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setTextColor(getContext().getResources().getColor(R.color.black)); 260 | alertDialog.setCancelable(false); 261 | } 262 | 263 | /** 264 | * 统计用户新增电视台 265 | */ 266 | private void statisticalAdd(String name) { 267 | // 统计按钮状态 268 | prop = new Properties(); 269 | prop.setProperty("电视台", name); 270 | StatService.trackCustomKVEvent(getContext(), "新增电视台", prop); 271 | } 272 | 273 | /** 274 | * 统计用户反馈 275 | */ 276 | private void statisticalFeedBack(String value) { 277 | // 统计按钮状态 278 | prop = new Properties(); 279 | prop.setProperty("内容", value); 280 | StatService.trackCustomKVEvent(getContext(), "反馈", prop); 281 | } 282 | 283 | interface OnRefreshListener { 284 | void onRefresh(); 285 | } 286 | 287 | public void setOnRefreshListener(OnRefreshListener onRefreshListener) { 288 | this.onRefreshListener = onRefreshListener; 289 | } 290 | } -------------------------------------------------------------------------------- /app/src/main/java/com/z/exoplayertest/view/WaveView.java: -------------------------------------------------------------------------------- 1 | package com.z.exoplayertest.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.CornerPathEffect; 7 | import android.graphics.Paint; 8 | import android.graphics.Path; 9 | import android.util.AttributeSet; 10 | import android.util.DisplayMetrics; 11 | import android.util.TypedValue; 12 | import android.view.View; 13 | 14 | import com.z.exoplayertest.R; 15 | 16 | import java.util.Random; 17 | 18 | import androidx.annotation.Nullable; 19 | 20 | public class WaveView extends View { 21 | private Paint mPaint; 22 | 23 | private Path mPath; 24 | 25 | private float mDrawHeight; 26 | 27 | private float mDrawWidth; 28 | 29 | private float amplitude[]; 30 | private float waveWidth; 31 | private float waveStart, waveEnd; 32 | private boolean isMax = true; 33 | private Context context; 34 | 35 | public WaveView(Context context, @Nullable AttributeSet attrs) { 36 | super(context, attrs); 37 | this.context = context; 38 | init(); 39 | } 40 | 41 | private void init() { 42 | mPath = new Path(); 43 | mPaint = new Paint(); 44 | mPaint.setAntiAlias(true); 45 | mPaint.setDither(true); 46 | mPaint.setStrokeWidth(1.5f); 47 | mPaint.setStyle(Paint.Style.STROKE); 48 | mPaint.setColor(context.getColor(R.color.colorTitle)); 49 | CornerPathEffect cornerPathEffect = new CornerPathEffect(300); 50 | mPaint.setPathEffect(cornerPathEffect); 51 | } 52 | 53 | @Override 54 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 55 | widthMeasureSpec = measureWidth(widthMeasureSpec); 56 | heightMeasureSpec = measureHeight(heightMeasureSpec); 57 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 58 | int paddingLeft = getPaddingLeft(); 59 | int paddingRight = getPaddingRight(); 60 | int paddingTop = getPaddingTop(); 61 | int paddingBottom = getPaddingBottom(); 62 | mDrawWidth = getMeasuredWidth() - paddingLeft - paddingRight; 63 | mDrawHeight = getMeasuredHeight() - paddingTop - paddingBottom; 64 | initOthers(); 65 | } 66 | 67 | public void setWaveWidth(float waveWidth, boolean isMax) { 68 | this.waveWidth = waveWidth; 69 | this.isMax = isMax; 70 | invalidate(); 71 | } 72 | 73 | private void initOthers() { 74 | waveStart = (mDrawWidth - waveWidth) / 2; 75 | waveEnd = waveStart + waveWidth; 76 | float mAmplitude = isMax ? mDrawHeight / 2 : mDrawHeight / 4; 77 | amplitude = new float[20]; 78 | Random random = new Random(); 79 | for (int i = 0; i < 20; i++) { 80 | if (i % 2 == 0) { 81 | amplitude[i] = mDrawHeight / 2 + (random.nextFloat() + 0.3f) * mAmplitude; 82 | } else { 83 | amplitude[i] = mDrawHeight / 2 - (random.nextFloat() + 0.3f) * mAmplitude; 84 | } 85 | } 86 | } 87 | 88 | private int measureWidth(int spec) { 89 | int mode = MeasureSpec.getMode(spec); 90 | if (mode == MeasureSpec.UNSPECIFIED) { 91 | DisplayMetrics dm = getResources().getDisplayMetrics(); 92 | int width = dm.widthPixels; 93 | spec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); 94 | } else if (mode == MeasureSpec.AT_MOST) { 95 | int value = MeasureSpec.getSize(spec); 96 | spec = MeasureSpec.makeMeasureSpec(value, MeasureSpec.EXACTLY); 97 | } 98 | return spec; 99 | } 100 | 101 | private int measureHeight(int spec) { 102 | int mode = MeasureSpec.getMode(spec); 103 | if (mode == MeasureSpec.EXACTLY) { 104 | return spec; 105 | } 106 | 107 | // 其他模式下的最大高度 108 | int height = (int) dip2px(50); 109 | 110 | if (mode == MeasureSpec.AT_MOST) { 111 | int preValue = MeasureSpec.getSize(spec); 112 | if (preValue < height) { 113 | height = preValue; 114 | } 115 | } 116 | spec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 117 | return spec; 118 | } 119 | 120 | private float dip2px(float dp) { 121 | DisplayMetrics dm = getResources().getDisplayMetrics(); 122 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, dm); 123 | } 124 | 125 | @Override 126 | protected void onDraw(Canvas canvas) { 127 | mPath.reset(); 128 | mPath.moveTo(0, mDrawHeight / 2); 129 | mPath.lineTo(waveStart, mDrawHeight / 2); 130 | //使前端直线慢慢过渡,不要太平滑 131 | for (int i = 0; i < 6; i++) { 132 | if (amplitude[0] > 0) { 133 | mPath.lineTo(waveStart, mDrawHeight / 2 + i * 2); 134 | } else { 135 | mPath.lineTo(waveStart, mDrawHeight / 2 - i * 2); 136 | } 137 | } 138 | 139 | for (int i = 0; i < amplitude.length; i++) { 140 | mPath.lineTo(waveStart + i * waveWidth / 20, amplitude[i]); 141 | } 142 | mPath.lineTo(waveEnd, mDrawHeight / 2); 143 | //使尾端直线慢慢过渡,不要太平滑 144 | for (int i = 0; i < 6; i++) { 145 | mPath.lineTo(waveEnd + i * 2, mDrawHeight / 2); 146 | } 147 | mPath.lineTo(mDrawWidth + 40, mDrawHeight / 2); 148 | canvas.drawPath(mPath, mPaint); 149 | } 150 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/edit_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/player_bottom_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ripple.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/search_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 21 | 22 | 31 | 32 | 33 | 36 | 37 | 48 | 49 | 56 | 57 | 58 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_select_tv.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 22 | 23 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/channel_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 19 | 26 | 27 | 28 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 14 | 23 | 24 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_feedback.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_channel.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 18 | 19 | 30 | 31 | 41 | 42 | 43 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_setting.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 17 | 18 | 24 | 25 | 31 | 32 | 41 | 42 | 43 | 51 | 52 | 59 | 60 | 68 | 69 | 76 | 77 | 78 | 84 | 85 | 93 | 94 | 99 | 100 | 108 | 109 | 116 | 117 | 118 | 124 | 125 | 134 | 135 | 140 | 141 | 149 | 150 | 157 | 158 | 159 | 165 | 166 | 175 | 176 | 181 | 182 | 189 | 190 | 191 | 198 | 199 | 208 | 209 | 214 | 215 | 223 | 224 | 231 | 232 | 233 | 234 | -------------------------------------------------------------------------------- /app/src/main/res/layout/playback_control_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 24 | 25 | 26 | 32 | 33 | 37 | 38 | 47 | 48 | 57 | 58 | 59 | 62 | 63 | 73 | 74 | 83 | 84 | 91 | 92 | 102 | 103 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /app/src/main/res/layout/tab_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 |

5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xhdpi/add.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xhdpi/back.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xhdpi/cancel.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xhdpi/delete.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/donation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xhdpi/donation.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/feedback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xhdpi/feedback.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xhdpi/full.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xhdpi/ic_launcher_black.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xhdpi/like.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xhdpi/pause.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xhdpi/play.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xhdpi/reset.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xhdpi/right.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/unlike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xhdpi/unlike.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xhdpi/update.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualC9/ExoPlayerTest/414dafe604763b78a1567d7829567af379685296/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-zh/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 轻TV 4 | 设置 5 | 请输入地址 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #C5C4C4 4 | #3D3D3D 5 | #F3F2F2 6 | #000000 7 | #C9C8C8 8 | #818080 9 | #FFFFFF 10 | #00FFFFFF 11 | #292929 12 | #F3F1F1 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 16dp 3 | 14sp 4 | 6sp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Light TV 3 | Settings 4 | Please enter url 5 | 通过百度搜索关键字:电视台名称+RTMP直播地址,然后把搜索到的以RTMP开头的链接拷贝到下方。 6 | 注:如果需要批量增加,则在手机根目录下找到channel.txt文件,如果没有则新建该文件,在文件内一个频道占一行,格式为:频道名称+&&+链接 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 |