├── .gitignore ├── .idea ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml └── runConfigurations.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── cwj │ │ └── updownshortvideo │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── cwj │ │ │ └── updownshortvideo │ │ │ ├── JzvdStdTikTok.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MyApp.kt │ │ │ ├── OnRecyViewListener.kt │ │ │ ├── RecyViewLayoutManager.kt │ │ │ ├── VideoAdapter.kt │ │ │ └── VideoBean.kt │ └── res │ │ ├── drawable-v24 │ │ ├── ic_launcher_foreground.xml │ │ └── tiktok_play_tiktok.png │ │ ├── drawable │ │ ├── ic_aixin.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_pl.xml │ │ └── ic_zhunfa.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── item_video.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── network_security_config.xml │ └── test │ └── java │ └── com │ └── cwj │ └── updownshortvideo │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── videocache ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src ├── androidTest └── java │ └── com │ └── danikula │ └── videocache │ └── ExampleInstrumentedTest.kt ├── main ├── AndroidManifest.xml └── java │ └── com │ └── danikula │ └── videocache │ ├── ByteArrayCache.java │ ├── ByteArraySource.java │ ├── Cache.java │ ├── CacheListener.java │ ├── Config.java │ ├── GetRequest.java │ ├── HttpProxyCache.java │ ├── HttpProxyCacheServer.java │ ├── HttpProxyCacheServerClients.java │ ├── HttpProxyPreLoader.java │ ├── HttpUrlSource.java │ ├── IgnoreHostProxySelector.java │ ├── InterruptedProxyCacheException.java │ ├── LogU.java │ ├── OkManager.java │ ├── Pinger.java │ ├── Preconditions.java │ ├── ProxyCache.java │ ├── ProxyCacheException.java │ ├── ProxyCacheUtils.java │ ├── Source.java │ ├── SourceInfo.java │ ├── StorageUtils.java │ ├── file │ ├── DiskUsage.java │ ├── FileCache.java │ ├── FileNameGenerator.java │ ├── Files.java │ ├── LruDiskUsage.java │ ├── Md5FileNameGenerator.java │ ├── TotalCountLruDiskUsage.java │ ├── TotalSizeLruDiskUsage.java │ └── UnlimitedDiskUsage.java │ ├── headers │ ├── EmptyHeadersInjector.java │ └── HeaderInjector.java │ └── sourcestorage │ ├── DatabaseSourceInfoStorage.java │ ├── NoSourceInfoStorage.java │ ├── SourceInfoStorage.java │ └── SourceInfoStorageFactory.java └── test └── java └── com └── danikula └── videocache └── ExampleUnitTest.kt /.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 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | updownshortvideo -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | xmlns:android 17 | 18 | ^$ 19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | xmlns:.* 28 | 29 | ^$ 30 | 31 | 32 | BY_NAME 33 | 34 |
35 |
36 | 37 | 38 | 39 | .*:id 40 | 41 | http://schemas.android.com/apk/res/android 42 | 43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 | .*:name 51 | 52 | http://schemas.android.com/apk/res/android 53 | 54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | name 62 | 63 | ^$ 64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | style 73 | 74 | ^$ 75 | 76 | 77 | 78 |
79 |
80 | 81 | 82 | 83 | .* 84 | 85 | ^$ 86 | 87 | 88 | BY_NAME 89 | 90 |
91 |
92 | 93 | 94 | 95 | .* 96 | 97 | http://schemas.android.com/apk/res/android 98 | 99 | 100 | ANDROID_ATTRIBUTE_ORDER 101 | 102 |
103 |
104 | 105 | 106 | 107 | .* 108 | 109 | .* 110 | 111 | 112 | BY_NAME 113 | 114 |
115 |
116 |
117 |
118 | 119 | 121 |
122 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # updownshortvideo 2 | 1.0 仿抖音上拉下滑短视频,加预缓存,接近秒开 3 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 30 7 | buildToolsVersion "30.0.2" 8 | 9 | defaultConfig { 10 | applicationId "com.cwj.updownshortvideo" 11 | minSdkVersion 16 12 | targetSdkVersion 30 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | 27 | compileOptions { 28 | sourceCompatibility JavaVersion.VERSION_1_8 29 | targetCompatibility JavaVersion.VERSION_1_8 30 | 31 | } 32 | 33 | dependencies { 34 | implementation fileTree(dir: "libs", include: ["*.jar"]) 35 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 36 | implementation 'androidx.core:core-ktx:1.3.1' 37 | implementation 'androidx.appcompat:appcompat:1.2.0' 38 | implementation 'androidx.constraintlayout:constraintlayout:2.0.1' 39 | testImplementation 'junit:junit:4.12' 40 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 41 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 42 | 43 | 44 | //适配器 45 | implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4' 46 | //播放器 47 | implementation 'cn.jzvd:jiaozivideoplayer:7.4.2' 48 | //图片加载 49 | implementation 'com.github.bumptech.glide:glide:4.11.0' 50 | annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' 51 | //缓存 52 | api project(":videocache") 53 | 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /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 | ##Glide 混淆 23 | -keep public class * implements com.bumptech.glide.module.GlideModule 24 | -keep class * extends com.bumptech.glide.module.AppGlideModule { 25 | (...); 26 | } 27 | -keep public enum com.bumptech.glide.load.ImageHeaderParser$** { 28 | **[] $VALUES; 29 | public *; 30 | } 31 | -keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder { 32 | *** rewind(); 33 | } 34 | 35 | # for DexGuard only 36 | -keepresourcexmlelements manifest/application/meta-data@value=GlideModule -------------------------------------------------------------------------------- /app/src/androidTest/java/com/cwj/updownshortvideo/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.cwj.updownshortvideo 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.cwj.updownshortvideo", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 25 | 26 | 27 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/cwj/updownshortvideo/JzvdStdTikTok.kt: -------------------------------------------------------------------------------- 1 | package com.cwj.updownshortvideo 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.util.Log 6 | import android.view.View 7 | import android.widget.ImageView 8 | import android.widget.Toast 9 | import cn.jzvd.JZUtils 10 | import cn.jzvd.Jzvd 11 | import cn.jzvd.JzvdStd 12 | 13 | /** 14 | * author : ChenWenJie 15 | * email : 1181620038@qq.com 16 | * date : 2020/9/22 17 | * desc : 重写播放器,方便控制。监听播放器状态。 18 | */ 19 | 20 | class JzvdStdTikTok : JzvdStd { 21 | constructor(context: Context?) : super(context) {} 22 | constructor(context: Context?, attrs: AttributeSet?) : super( 23 | context, 24 | attrs 25 | ) { 26 | } 27 | 28 | override fun init(context: Context?) { 29 | super.init(context) 30 | bottomContainer.visibility = View.GONE 31 | currentTimeTextView.visibility = View.GONE 32 | totalTimeTextView.visibility = View.GONE //当前时间 33 | fullscreenButton.visibility = View.GONE //放大按钮 34 | topContainer.visibility = View.GONE 35 | progressBar.visibility = View.GONE //控制的 36 | loadingProgressBar.visibility = View.GONE //加载loaing 37 | bottomProgressBar.visibility = View.VISIBLE //最底部的进度 38 | posterImageView.scaleType = ImageView.ScaleType.FIT_CENTER 39 | } 40 | 41 | override fun setUp( 42 | url: String?, 43 | title: String?, 44 | screen: Int, 45 | mediaInterfaceClass: Class<*>? 46 | ) { 47 | super.setUp(url, title, screen, mediaInterfaceClass) 48 | } 49 | 50 | //changeUiTo 真能能修改ui的方法 51 | override fun changeUiToNormal() { 52 | super.changeUiToNormal() 53 | bottomContainer.visibility = View.GONE 54 | topContainer.visibility = View.GONE 55 | // mDialogProgressBar.setVisibility(GONE); 56 | } 57 | 58 | override fun setAllControlsVisiblity( 59 | topCon: Int, bottomCon: Int, startBtn: Int, loadingPro: Int, 60 | posterImg: Int, bottomPro: Int, retryLayout: Int 61 | ) { 62 | topContainer.visibility = topCon 63 | bottomContainer.visibility = bottomCon 64 | startButton.visibility = startBtn 65 | loadingProgressBar.visibility = View.GONE 66 | posterImageView.visibility = posterImg 67 | bottomProgressBar.visibility = View.VISIBLE 68 | mRetryLayout.visibility = retryLayout 69 | } 70 | 71 | override fun dissmissControlView() { 72 | if (state != Jzvd.STATE_NORMAL && state != Jzvd.STATE_ERROR && state != Jzvd.STATE_AUTO_COMPLETE 73 | ) { 74 | post { 75 | bottomContainer.visibility = View.INVISIBLE 76 | topContainer.visibility = View.INVISIBLE 77 | startButton.visibility = View.INVISIBLE 78 | if (clarityPopWindow != null) { 79 | clarityPopWindow.dismiss() 80 | } 81 | if (screen != Jzvd.SCREEN_TINY) { 82 | bottomProgressBar.visibility = View.GONE 83 | } 84 | } 85 | } 86 | } 87 | 88 | override fun onClickUiToggle() { 89 | super.onClickUiToggle() 90 | Log.i(Jzvd.TAG, "click blank") 91 | startButton.performClick() 92 | bottomContainer.visibility = View.GONE 93 | topContainer.visibility = View.GONE 94 | } 95 | 96 | override fun onStateNormal() { 97 | super.onStateNormal() 98 | } 99 | 100 | override fun onStatePreparing() { 101 | super.onStatePreparing() 102 | Log.e("onStatePreparing", " 准备") 103 | } 104 | 105 | override fun onStatePlaying() { 106 | super.onStatePlaying() 107 | val times = duration 108 | Log.e("onStatePlaying", " $times") 109 | } 110 | 111 | override fun onStatePause() { 112 | super.onStatePause() 113 | Log.e("onStatePause:", "暂停") 114 | } 115 | 116 | override fun onStateError() { 117 | super.onStateError() 118 | Log.e("onStateError:", "错误") 119 | } 120 | 121 | //播放完成自动播放 122 | override fun onStateAutoComplete() { 123 | Toast.makeText(applicationContext, "onStateAutoComplete", Toast.LENGTH_SHORT).show() 124 | } 125 | 126 | override fun onCompletion() { 127 | // RxBus.INSTANCE.post(RXCmCWJ(121)) 128 | } 129 | 130 | 131 | override fun updateStartImage() { 132 | if (state == Jzvd.STATE_PLAYING) { 133 | startButton.visibility = View.VISIBLE 134 | startButton.setImageResource(R.drawable.tiktok_play_tiktok) 135 | replayTextView.visibility = View.GONE 136 | } else if (state == Jzvd.STATE_ERROR) { 137 | startButton.visibility = View.INVISIBLE 138 | replayTextView.visibility = View.GONE 139 | } else if (state == Jzvd.STATE_AUTO_COMPLETE) { 140 | startButton.visibility = View.VISIBLE 141 | startButton.setImageResource(R.drawable.tiktok_play_tiktok) 142 | replayTextView.visibility = View.GONE 143 | } else { 144 | startButton.setImageResource(R.drawable.tiktok_play_tiktok) 145 | replayTextView.visibility = View.GONE 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /app/src/main/java/com/cwj/updownshortvideo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.cwj.updownshortvideo 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import android.view.View 6 | import androidx.recyclerview.widget.OrientationHelper 7 | import androidx.recyclerview.widget.RecyclerView 8 | import cn.jzvd.Jzvd 9 | import kotlinx.android.synthetic.main.activity_main.* 10 | 11 | /** 12 | * author : ChenWenJie 13 | * email : 1181620038@qq.com 14 | * date : 2020/9/22 15 | * desc : 上下滑动 播放短视频。 16 | */ 17 | 18 | class MainActivity : AppCompatActivity() { 19 | lateinit var adapter: VideoAdapter 20 | private var mCurrentPosition = -1 21 | var videos = ArrayList(); 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | setContentView(R.layout.activity_main) 26 | initData() 27 | initView() 28 | } 29 | 30 | 31 | fun initView() { 32 | var recyViewLayoutManager = RecyViewLayoutManager( 33 | this, OrientationHelper.VERTICAL 34 | ) 35 | recy.layoutManager = recyViewLayoutManager 36 | adapter = VideoAdapter(this) 37 | recy.adapter = adapter 38 | //预加载下一个 39 | adapter.setNewData(videos) 40 | //指定位置其他页面跳转过来。直接定位指定posion 这里不需要。 41 | // recy.scrollToPosition(postion); 42 | 43 | recyViewLayoutManager.setOnViewPagerListener(object : OnRecyViewListener { 44 | override fun onInitComplete() { 45 | //初始化 自动播放 46 | autoPlayVideo() 47 | 48 | } 49 | 50 | override fun onPageRelease(isNext: Boolean, position: Int) { 51 | //滑动时,释放上一个 52 | if (mCurrentPosition == position) { 53 | Jzvd.releaseAllVideos() 54 | 55 | } 56 | } 57 | 58 | override fun onPageSelected(position: Int, isBottom: Boolean) { 59 | //滑动后的当前Item ,具体自行打印 60 | if (mCurrentPosition == position) { 61 | return 62 | } 63 | if (isBottom) { 64 | //是最底部,执行加载更多数据 65 | loadData() 66 | } 67 | autoPlayVideo() 68 | mCurrentPosition = position 69 | } 70 | 71 | }) 72 | 73 | ///监听item离开了屏幕 74 | recy.addOnChildAttachStateChangeListener(object : 75 | RecyclerView.OnChildAttachStateChangeListener { 76 | override fun onChildViewDetachedFromWindow(view: View) { 77 | val jzvd: Jzvd = view.findViewById(R.id.jz_video) 78 | if (jzvd != null && Jzvd.CURRENT_JZVD != null && 79 | jzvd.jzDataSource.containsTheUrl(Jzvd.CURRENT_JZVD.jzDataSource.currentUrl) 80 | ) { 81 | if (Jzvd.CURRENT_JZVD != null && Jzvd.CURRENT_JZVD.screen != Jzvd.SCREEN_FULLSCREEN) { 82 | Jzvd.releaseAllVideos() 83 | } 84 | } 85 | } 86 | 87 | override fun onChildViewAttachedToWindow(view: View) { 88 | } 89 | 90 | }) 91 | } 92 | 93 | fun initData() { 94 | videos.add( 95 | VideoBean( 96 | 1, 97 | "中韩夫妇与两宝", 98 | "https://p29-dy.byteimg.com/aweme/100x100/2f9480001ea8cc615d6a9.jpeg?from=401053103", 99 | "家有一老如有一宝,不分国界", 100 | "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200fe90000bs854d21rirdcdon9fk0&ratio=720p&line=0", 101 | "https://p6-dy-ipv6.byteimg.com/img/tos-cn-p-0015/2263f31dfb304120a5fb5d6655b230c5_1594905185~tplv-dmt-logom:tos-cn-i-0813/65302cd29d2d4043a98de10a7723d33d.image?from=2563711402_large" 102 | ) 103 | ) 104 | videos.add( 105 | VideoBean( 106 | 2, 107 | "央视新闻", 108 | "https://p6-dy-ipv6.byteimg.com/aweme/100x100/30e520009a01cad2d810e.jpeg?from=4010531038", 109 | "高三考生注意了!今年高考时间推迟一个月,为7月7日至7月8日", 110 | "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200fe90000bs854d21rirdcdon9fk0&ratio=720p&line=0", 111 | "https://p29-dy.byteimg.com/obj/tos-cn-p-0015/f79a5fe204e24bbbbfc55a76a81f5c2c_1585626014?from=2563711402_large" 112 | ) 113 | ) 114 | videos.add( 115 | VideoBean( 116 | 3, 117 | "查查和张张", 118 | "https://p6-dy-ipv6.byteimg.com/aweme/100x100/30e520009a01cad2d810e.jpeg?from=4010531038", 119 | "#情侣 #春节 就没有我解决不了的婆媳关系 嘿嘿@Singing哥 @抖音小助手", 120 | "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f670000bomln43d82dvbadk5a00&ratio=720p&line=0", 121 | "https://p29-dy.byteimg.com/obj/tos-cn-p-0015/f61dcb8127204a8cb7a322bf816c0b3e_1580030882?from=2563711402_large" 122 | ) 123 | ) 124 | videos.add( 125 | VideoBean( 126 | 4, 127 | "大鹏(宇宙简史)", 128 | "https://p3-dy-ipv6.byteimg.com/aweme/100x100/1e1170002093b2ff1d0f7.jpeg?from=4010531038", 129 | "2018下半年所以天文奇观!还有流星雨哦!千万不要错过了", 130 | "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f4e0000bddrstnff778g23hs6mg&ratio=720p&line=0", 131 | "https://p6-dy-ipv6.byteimg.com/obj/9dfb0003c8c228b763eb?from=2563711402_large" 132 | ) 133 | ) 134 | videos.add( 135 | VideoBean( 136 | 5, 137 | "胖爹带娃", 138 | "https://p3-dy-ipv6.byteimg.com/aweme/100x100/26ec600005035c9b87288.jpeg?from=4010531038", 139 | "带孩子出门玩耍一定要记得这一点!接力下去,别以为危险离我们很远@抖音小助手 #暑假安全", 140 | "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f4e0000bddrstnff778g23hs6mg&ratio=720p&line=0", 141 | "https://p9-dy.byteimg.com/obj/2c5c600050a7b42352869?from=2563711402_large" 142 | ) 143 | ) 144 | 145 | } 146 | //加载 147 | fun loadData() { 148 | videos.add( 149 | VideoBean( 150 | 6, 151 | "遵义观察 ", 152 | "https://p9-dy.byteimg.com/aweme/100x100/2e1ce00021ee51a2aacdc.jpeg?from=4010531038", 153 | "老伴咱走!", 154 | "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200fe10000bloib0nrri6bf7b1k4fg&ratio=720p&line=0", 155 | "https://p29-dy.byteimg.com/obj/tos-cn-p-0015/601c6d730167431184c5412e81cd32d1?from=2563711402_large" 156 | ) 157 | ) 158 | videos.add( 159 | VideoBean( 160 | 7, 161 | "BTV养生堂 ", 162 | "https://p6-dy-ipv6.byteimg.com/aweme/100x100/3151700027839b153b924.jpeg?from=4010531038", 163 | "湿气过重,快收藏这个中医调理方!!", 164 | "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f730000bpebr0dqg5balrfhqlog&ratio=720p&line=0", 165 | "https://p3-dy-ipv6.byteimg.com/obj/tos-cn-p-0015/0e99f0aca9764e7da53be1096a3bd641_1583136211?from=2563711402_large" 166 | ) 167 | ) 168 | 169 | videos.add( 170 | VideoBean( 171 | 8, 172 | "河南都市频道 ", 173 | "https://p9-dy.byteimg.com/aweme/100x100/312a8000720705660b806.jpeg?from=4010531038", 174 | "痛心!手扶梯绞断女童两根手指!带娃搭扶梯的一定要注意了!(上)!", 175 | "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f250000bgsu5vamac2seo2gp53g&ratio=720p&line=0", 176 | "https://p1-dy-ipv6.byteimg.com/obj/160b4000aa3f373bd14cd?from=2563711402_large" 177 | ) 178 | ) 179 | 180 | videos.add( 181 | VideoBean( 182 | 9, 183 | "科学小妙招 ", 184 | "https://p26-dy.byteimg.com/aweme/100x100/312090000434b4dd10244.jpeg?from=4010531038", 185 | "不喜欢的衣服扔了可惜,这样改造一下非常酷#生活小妙招 #生活小技巧 #废物利用 @抖音小助手", 186 | "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200fe10000bq2bf7s9hq5lufbuoflg&ratio=720p&line=0", 187 | "https://p9-dy.byteimg.com/obj/tos-cn-p-0015/9a6784fcbf9b43849081d7a3388db08d_1585756125?from=2563711402_large" 188 | ) 189 | ) 190 | videos.add( 191 | VideoBean( 192 | 10, 193 | "一起装修网 ", 194 | "https://p29-dy.byteimg.com/aweme/100x100/f77d000eae902034a2bf.jpeg?from=4010531038", 195 | "#装修 #黑幕重重 怎样选购浴室柜?揭露浴室柜增项(增项:镜子,水龙头,软管等)@胡 一刀", 196 | "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f9a0000belnavkqn5hfpb70b5kg&ratio=720p&line=0", 197 | "https://p29-dy.byteimg.com/obj/c8f200068c30b23f1024?from=2563711402_large" 198 | ) 199 | ) 200 | 201 | } 202 | 203 | 204 | /** 205 | * 滑动后自动播放。 206 | */ 207 | private fun autoPlayVideo() { 208 | if (recy == null || recy.getChildAt(0) == null) { 209 | return 210 | } 211 | val player: JzvdStdTikTok = recy.getChildAt(0).findViewById(R.id.jz_video) 212 | if (player != null) { 213 | player.startVideoAfterPreloading() 214 | //播放开始,进行倒计时 215 | 216 | } 217 | } 218 | 219 | 220 | override fun onBackPressed() { 221 | if (Jzvd.backPress()) { 222 | return 223 | } 224 | super.onBackPressed() 225 | 226 | } 227 | 228 | override fun onPause() { 229 | super.onPause() 230 | Jzvd.releaseAllVideos() 231 | 232 | } 233 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cwj/updownshortvideo/MyApp.kt: -------------------------------------------------------------------------------- 1 | package com.cwj.updownshortvideo 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.cwj.updownshortvideo.SSlUtiles.TrustAllHostnameVerifier 6 | import com.danikula.videocache.HttpProxyCacheServer 7 | import javax.net.ssl.HttpsURLConnection 8 | 9 | /** 10 | * author : ChenWenJie 11 | * email :1181620038@qq.com 12 | * date : 2020/9/22 13 | * desc : 14 | */ 15 | class MyApp : Application() { 16 | //缓存代理服务 17 | private var proxy: HttpProxyCacheServer? = null 18 | override fun onCreate() { 19 | super.onCreate() 20 | 21 | } 22 | //获取缓存代理。 23 | object StaticParams{ 24 | fun getProxy(context: Context): HttpProxyCacheServer? { 25 | var app: MyApp = context.applicationContext as MyApp 26 | return if (app.proxy == null) app.newProxy().also({ app.proxy = it }) else app.proxy 27 | } 28 | // 29 | } 30 | 31 | 32 | 33 | private fun newProxy(): HttpProxyCacheServer? { 34 | return HttpProxyCacheServer.Builder(this) 35 | .maxCacheSize(1024 * 1024 * 1024) 36 | .maxCacheFilesCount(30) 37 | .build(); 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/cwj/updownshortvideo/OnRecyViewListener.kt: -------------------------------------------------------------------------------- 1 | package com.cwj.updownshortvideo 2 | 3 | /** 4 | * author : ChenWenJie 5 | * email : 1181620038@qq.com 6 | * date : 2020/9/22 7 | * desc : 监听接口 8 | */ 9 | interface OnRecyViewListener { 10 | /*初始化完成*/ 11 | fun onInitComplete() 12 | 13 | /*释放的监听*/ 14 | fun onPageRelease(isNext: Boolean, position: Int) 15 | 16 | /*选中的监听以及判断是否滑动到底部*/ 17 | fun onPageSelected(position: Int, isBottom: Boolean) 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cwj/updownshortvideo/RecyViewLayoutManager.kt: -------------------------------------------------------------------------------- 1 | package com.cwj.updownshortvideo 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import androidx.recyclerview.widget.LinearLayoutManager 6 | import androidx.recyclerview.widget.PagerSnapHelper 7 | import androidx.recyclerview.widget.RecyclerView 8 | 9 | /** 10 | * author : ChenWenJie 11 | * email : 1181620038@qq.com 12 | * date : 2020/9/22 13 | * desc : 定义管理器,一item一屏 监听滑动状态 14 | */ 15 | 16 | class RecyViewLayoutManager : LinearLayoutManager { 17 | private var mPagerSnapHelper: PagerSnapHelper? = null 18 | private var mOnRecycleViewListener: OnRecyViewListener? = null 19 | private var mRecyclerView: RecyclerView? = null 20 | private var mDrift //位移,用来判断移动方向 21 | = 0 22 | 23 | 24 | private val mChildAttachStateChangeListener: RecyclerView.OnChildAttachStateChangeListener = 25 | object : RecyclerView.OnChildAttachStateChangeListener { 26 | override fun onChildViewDetachedFromWindow(view: View) { 27 | if (mDrift >= 0) { 28 | if (mOnRecycleViewListener != null) { 29 | mOnRecycleViewListener!!.onPageRelease(true, getPosition(view)) 30 | } 31 | } else { 32 | if (mOnRecycleViewListener != null) { 33 | mOnRecycleViewListener!!.onPageRelease(false, getPosition(view)) 34 | } 35 | } 36 | } 37 | 38 | override fun onChildViewAttachedToWindow(view: View) { 39 | if (mOnRecycleViewListener != null && getChildCount() === 1) { 40 | mOnRecycleViewListener!!.onInitComplete() 41 | } 42 | } 43 | } 44 | 45 | constructor(context: Context?, orientation: Int) : super( 46 | context, 47 | orientation, 48 | false 49 | ) { 50 | init() 51 | } 52 | 53 | constructor( 54 | context: Context?, 55 | orientation: Int, 56 | reverseLayout: Boolean 57 | ) : super(context, orientation, reverseLayout) { 58 | init() 59 | } 60 | 61 | private fun init() { 62 | mPagerSnapHelper = PagerSnapHelper() 63 | } 64 | 65 | override fun onAttachedToWindow(view: RecyclerView?) { 66 | super.onAttachedToWindow(view) 67 | mPagerSnapHelper?.attachToRecyclerView(view) 68 | mRecyclerView = view 69 | mRecyclerView?.addOnChildAttachStateChangeListener(mChildAttachStateChangeListener) 70 | } 71 | 72 | override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) { 73 | super.onLayoutChildren(recycler, state) 74 | // 75 | } 76 | 77 | /** 78 | * 滑动状态的改变 79 | * 缓慢拖拽-> SCROLL_STATE_DRAGGING 80 | * 快速滚动-> SCROLL_STATE_SETTLING 81 | * 空闲状态-> SCROLL_STATE_IDLE 82 | * 83 | * @param state 84 | */ 85 | override fun onScrollStateChanged(state: Int) { 86 | when (state) { 87 | RecyclerView.SCROLL_STATE_IDLE -> { 88 | val viewIdle: View = mPagerSnapHelper!!.findSnapView(this)!! 89 | val positionIdle: Int = getPosition(viewIdle) 90 | if (mOnRecycleViewListener != null && getChildCount() === 1) { 91 | mOnRecycleViewListener!!.onPageSelected( 92 | positionIdle, 93 | positionIdle == getItemCount() - 1 94 | ) 95 | } 96 | } 97 | RecyclerView.SCROLL_STATE_DRAGGING -> { 98 | val viewDrag: View? = mPagerSnapHelper!!.findSnapView(this)!! 99 | val positionDrag: Int = getPosition(viewDrag!!) 100 | } 101 | RecyclerView.SCROLL_STATE_SETTLING -> { 102 | val viewSettling: View = mPagerSnapHelper!!.findSnapView(this)!! 103 | val positionSettling: Int = getPosition(viewSettling) 104 | } 105 | } 106 | } 107 | 108 | /** 109 | * 监听竖直方向的相对偏移量 110 | * 111 | * @param dy 112 | * @param recycler 113 | * @param state 114 | * @return 115 | */ 116 | override fun scrollVerticallyBy( 117 | dy: Int, 118 | recycler: RecyclerView.Recycler?, 119 | state: RecyclerView.State? 120 | ): Int { 121 | mDrift = dy 122 | return super.scrollVerticallyBy(dy, recycler, state) 123 | } 124 | 125 | /** 126 | * 监听水平方向的相对偏移量 127 | * 128 | * @param dx 129 | * @param recycler 130 | * @param state 131 | * @return 132 | */ 133 | override fun scrollHorizontallyBy( 134 | dx: Int, 135 | recycler: RecyclerView.Recycler?, 136 | state: RecyclerView.State? 137 | ): Int { 138 | mDrift = dx 139 | return super.scrollHorizontallyBy(dx, recycler, state) 140 | } 141 | 142 | /** 143 | * 设置监听 144 | * 145 | * @param listener 146 | */ 147 | fun setOnViewPagerListener(listener: OnRecyViewListener?) { 148 | mOnRecycleViewListener = listener 149 | } 150 | 151 | companion object { 152 | private const val TAG = "ViewPagerLayoutManager" 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /app/src/main/java/com/cwj/updownshortvideo/VideoAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.cwj.updownshortvideo 2 | 3 | import android.app.Activity 4 | import android.net.Uri 5 | import android.util.Log 6 | import android.widget.ImageView 7 | import cn.jzvd.JZUtils 8 | import cn.jzvd.Jzvd 9 | import cn.jzvd.JzvdStd 10 | import com.bumptech.glide.Glide 11 | import com.bumptech.glide.request.RequestOptions 12 | import com.chad.library.adapter.base.BaseQuickAdapter 13 | import com.chad.library.adapter.base.viewholder.BaseViewHolder 14 | import com.cwj.updownshortvideo.MyApp.StaticParams.getProxy 15 | 16 | /** 17 | * author : ChenWenJie 18 | * email : 1181620038@qq.com 19 | * date : 2020/9/22 20 | * desc : 适配器 21 | */ 22 | class VideoAdapter(var activity: Activity) : 23 | BaseQuickAdapter(R.layout.item_video) { 24 | override fun convert(holder: BaseViewHolder, item: VideoBean) { 25 | //圆形用户头像 26 | val requestOptions = RequestOptions.circleCropTransform() 27 | Glide.with(activity).load(item.user_image).apply(requestOptions) 28 | .into(holder.getView(R.id.user_iv)) 29 | //用户名 30 | holder.setText(R.id.username_tv, item.user_name) 31 | //标题 32 | holder.setText(R.id.usertitle_tv, item.video_title) 33 | //缩略图 34 | Glide.with(activity).load(item.video_image) 35 | .into(holder?.getView(R.id.jz_video)!!.posterImageView) 36 | 37 | //声明 代理服务缓存 38 | val proxy = getProxy(activity) 39 | //这个缓存下一个 40 | if (holder?.layoutPosition!! + 1 < itemCount) { 41 | var item1 = getItem(holder?.layoutPosition!! + 1) 42 | //缓存下一个 10秒 43 | proxy!!.preLoad(item1!!.video_path, 10) 44 | } 45 | 46 | //缓存当前,播放当前 47 | var proxyUrl =proxy?.getProxyUrl(item.video_path).toString() //设置视 48 | 49 | 50 | 51 | setPlay(holder.getView(R.id.jz_video),proxyUrl) 52 | } 53 | 54 | 55 | fun setPlay(jzvdStdTikTok: JzvdStdTikTok, path: String) { 56 | Log.e("VideoAdapter", "${path}") 57 | //不保存播放进度 58 | Jzvd.SAVE_PROGRESS = false 59 | //取消播放时在非WIFIDialog提示 60 | Jzvd.WIFI_TIP_DIALOG_SHOWED = true 61 | // 清除某个URL进度 62 | //JZUtils.clearSavedProgress(activity, path) 63 | jzvdStdTikTok!!.setUp(path, "", JzvdStd.SCREEN_FULLSCREEN) 64 | 65 | } 66 | 67 | } 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/cwj/updownshortvideo/VideoBean.kt: -------------------------------------------------------------------------------- 1 | package com.cwj.updownshortvideo 2 | 3 | /** 4 | * author : ChenWenJie 5 | * email : 1181620038@qq.com 6 | * date : 2020/9/22 7 | * desc : 视频实体类 8 | */ 9 | class VideoBean { 10 | // ID 11 | var id: Int = 0 12 | 13 | //用户名 14 | var user_name: String = "" 15 | 16 | //用户头像 17 | var user_image: String = "" 18 | 19 | //视频标题 20 | var video_title: String = "" 21 | 22 | //视频路径 23 | var video_path: String = "" 24 | 25 | //视频图片 26 | var video_image: String = "" 27 | 28 | constructor( 29 | id: Int, 30 | user_name: String, 31 | user_image: String, 32 | video_title: String, 33 | video_path: String, 34 | video_image: String 35 | ) { 36 | this.id = id 37 | this.user_name = user_name 38 | this.user_image = user_image 39 | this.video_title = video_title 40 | this.video_path = video_path 41 | this.video_image = video_image 42 | } 43 | } 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/tiktok_play_tiktok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o199666/updownshortvideo/aea507a9789f0ad8e8555921850b79ede0e8852b/app/src/main/res/drawable-v24/tiktok_play_tiktok.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_aixin.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pl.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_zhunfa.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_video.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 26 | 27 | 33 | 34 | 45 | 46 | 47 | 61 | 68 | 69 | 70 | 71 | 78 | 79 | 87 | 88 | 94 | 95 | 103 | 109 | 110 | 116 | 117 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o199666/updownshortvideo/aea507a9789f0ad8e8555921850b79ede0e8852b/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o199666/updownshortvideo/aea507a9789f0ad8e8555921850b79ede0e8852b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o199666/updownshortvideo/aea507a9789f0ad8e8555921850b79ede0e8852b/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o199666/updownshortvideo/aea507a9789f0ad8e8555921850b79ede0e8852b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o199666/updownshortvideo/aea507a9789f0ad8e8555921850b79ede0e8852b/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o199666/updownshortvideo/aea507a9789f0ad8e8555921850b79ede0e8852b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o199666/updownshortvideo/aea507a9789f0ad8e8555921850b79ede0e8852b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o199666/updownshortvideo/aea507a9789f0ad8e8555921850b79ede0e8852b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o199666/updownshortvideo/aea507a9789f0ad8e8555921850b79ede0e8852b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o199666/updownshortvideo/aea507a9789f0ad8e8555921850b79ede0e8852b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | updownshortvideo 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/test/java/com/cwj/updownshortvideo/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.cwj.updownshortvideo 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = "1.3.72" 4 | repositories { 5 | google() 6 | jcenter() 7 | } 8 | dependencies { 9 | classpath "com.android.tools.build:gradle:4.0.1" 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | } 22 | } 23 | 24 | task clean(type: Delete) { 25 | delete rootProject.buildDir 26 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o199666/updownshortvideo/aea507a9789f0ad8e8555921850b79ede0e8852b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Sep 22 10:53:27 CST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':videocache' 2 | include ':app' 3 | rootProject.name = "updownshortvideo" -------------------------------------------------------------------------------- /videocache/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /videocache/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | //apply plugin: 'com.github.dcendents.android-maven' 5 | //group='com.github.MJLblabla' 6 | android { 7 | compileSdkVersion 29 8 | buildToolsVersion "29.0.3" 9 | 10 | defaultConfig { 11 | minSdkVersion 16 12 | targetSdkVersion 29 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | consumerProguardFiles 'consumer-rules.pro' 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | 27 | } 28 | 29 | dependencies { 30 | implementation fileTree(dir: 'libs', include: ['*.jar']) 31 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 32 | 33 | testImplementation 'junit:junit:4.12' 34 | implementation 'org.conscrypt:conscrypt-android:2.2.1' 35 | implementation("com.squareup.okhttp3:okhttp:4.4.0") 36 | 37 | } 38 | -------------------------------------------------------------------------------- /videocache/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o199666/updownshortvideo/aea507a9789f0ad8e8555921850b79ede0e8852b/videocache/consumer-rules.pro -------------------------------------------------------------------------------- /videocache/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 | -------------------------------------------------------------------------------- /videocache/src/androidTest/java/com/danikula/videocache/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.danikula.videocache.test", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /videocache/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/ByteArrayCache.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.util.Arrays; 5 | 6 | /** 7 | * Simple memory based {@link Cache} implementation. 8 | * 9 | * @author Alexey Danilov (danikula@gmail.com). 10 | */ 11 | public class ByteArrayCache implements Cache { 12 | 13 | private volatile byte[] data; 14 | private volatile boolean completed; 15 | 16 | public ByteArrayCache() { 17 | this(new byte[0]); 18 | } 19 | 20 | public ByteArrayCache(byte[] data) { 21 | this.data = Preconditions.checkNotNull(data); 22 | } 23 | 24 | @Override 25 | public int read(byte[] buffer, long offset, int length) throws ProxyCacheException { 26 | if (offset >= data.length) { 27 | return -1; 28 | } 29 | if (offset > Integer.MAX_VALUE) { 30 | throw new IllegalArgumentException("Too long offset for memory cache " + offset); 31 | } 32 | return new ByteArrayInputStream(data).read(buffer, (int) offset, length); 33 | } 34 | 35 | @Override 36 | public long available() throws ProxyCacheException { 37 | return data.length; 38 | } 39 | 40 | @Override 41 | public void append(byte[] newData, int length) throws ProxyCacheException { 42 | Preconditions.checkNotNull(data); 43 | Preconditions.checkArgument(length >= 0 && length <= newData.length); 44 | 45 | byte[] appendedData = Arrays.copyOf(data, data.length + length); 46 | System.arraycopy(newData, 0, appendedData, data.length, length); 47 | data = appendedData; 48 | } 49 | 50 | @Override 51 | public void close() throws ProxyCacheException { 52 | } 53 | 54 | @Override 55 | public void complete() { 56 | completed = true; 57 | } 58 | 59 | @Override 60 | public boolean isCompleted() { 61 | return completed; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/ByteArraySource.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import java.io.ByteArrayInputStream; 4 | 5 | /** 6 | * Simple memory based {@link Source} implementation. 7 | * 8 | * @author Alexey Danilov (danikula@gmail.com). 9 | */ 10 | public class ByteArraySource implements Source { 11 | 12 | private final byte[] data; 13 | private ByteArrayInputStream arrayInputStream; 14 | 15 | public ByteArraySource(byte[] data) { 16 | this.data = data; 17 | } 18 | 19 | @Override 20 | public int read(byte[] buffer) throws ProxyCacheException { 21 | return arrayInputStream.read(buffer, 0, buffer.length); 22 | } 23 | 24 | @Override 25 | public long length() throws ProxyCacheException { 26 | return data.length; 27 | } 28 | 29 | @Override 30 | public void open(long offset) throws ProxyCacheException { 31 | arrayInputStream = new ByteArrayInputStream(data); 32 | arrayInputStream.skip(offset); 33 | } 34 | 35 | @Override 36 | public void close() throws ProxyCacheException { 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/Cache.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | /** 4 | * Cache for proxy. 5 | * 6 | * @author Alexey Danilov (danikula@gmail.com). 7 | */ 8 | public interface Cache { 9 | 10 | long available() throws ProxyCacheException; 11 | 12 | int read(byte[] buffer, long offset, int length) throws ProxyCacheException; 13 | 14 | void append(byte[] data, int length) throws ProxyCacheException; 15 | 16 | void close() throws ProxyCacheException; 17 | 18 | void complete() throws ProxyCacheException; 19 | 20 | boolean isCompleted(); 21 | } 22 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/CacheListener.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import java.io.File; 4 | 5 | /** 6 | * Listener for cache availability. 7 | * 8 | * @author Egor Makovsky (yahor.makouski@gmail.com) 9 | * @author Alexey Danilov (danikula@gmail.com). 10 | */ 11 | public interface CacheListener { 12 | 13 | void onCacheAvailable(File cacheFile, String url, int percentsAvailable); 14 | } 15 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/Config.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import com.danikula.videocache.file.DiskUsage; 4 | import com.danikula.videocache.file.FileNameGenerator; 5 | import com.danikula.videocache.headers.HeaderInjector; 6 | import com.danikula.videocache.sourcestorage.SourceInfoStorage; 7 | 8 | import java.io.File; 9 | 10 | /** 11 | * Configuration for proxy cache. 12 | * 13 | * @author Alexey Danilov (danikula@gmail.com). 14 | */ 15 | class Config { 16 | 17 | public final File cacheRoot; 18 | public final FileNameGenerator fileNameGenerator; 19 | public final DiskUsage diskUsage; 20 | public final SourceInfoStorage sourceInfoStorage; 21 | public final HeaderInjector headerInjector; 22 | 23 | Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage, SourceInfoStorage sourceInfoStorage, HeaderInjector headerInjector) { 24 | this.cacheRoot = cacheRoot; 25 | this.fileNameGenerator = fileNameGenerator; 26 | this.diskUsage = diskUsage; 27 | this.sourceInfoStorage = sourceInfoStorage; 28 | this.headerInjector = headerInjector; 29 | } 30 | 31 | File generateCacheFile(String url) { 32 | String name = fileNameGenerator.generate(url); 33 | return new File(cacheRoot, name); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/GetRequest.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import android.text.TextUtils; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.InputStreamReader; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | import static com.danikula.videocache.Preconditions.checkNotNull; 13 | 14 | /** 15 | * Model for Http GET request. 16 | * 17 | * @author Alexey Danilov (danikula@gmail.com). 18 | */ 19 | class GetRequest { 20 | 21 | private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile("[R,r]ange:[ ]?bytes=(\\d*)-"); 22 | private static final Pattern URL_PATTERN = Pattern.compile("GET /(.*) HTTP"); 23 | 24 | public final String uri; 25 | public final long rangeOffset; 26 | public final boolean partial; 27 | 28 | /** 29 | * 预下载 30 | */ 31 | public boolean isPreLoad = false; 32 | /** 33 | * 预下载百分比 34 | */ 35 | public int percentsPreLoad = 4; 36 | 37 | 38 | 39 | public GetRequest(String request) { 40 | checkNotNull(request); 41 | long offset = findRangeOffset(request); 42 | this.rangeOffset = Math.max(0, offset); 43 | this.partial = offset >= 0; 44 | this.uri = findUri(request); 45 | } 46 | 47 | public static GetRequest read(InputStream inputStream) throws IOException { 48 | BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); 49 | StringBuilder stringRequest = new StringBuilder(); 50 | String line; 51 | while (!TextUtils.isEmpty(line = reader.readLine())) { // until new line (headers ending) 52 | stringRequest.append(line).append('\n'); 53 | } 54 | return new GetRequest(stringRequest.toString()); 55 | } 56 | 57 | private long findRangeOffset(String request) { 58 | Matcher matcher = RANGE_HEADER_PATTERN.matcher(request); 59 | if (matcher.find()) { 60 | String rangeValue = matcher.group(1); 61 | return Long.parseLong(rangeValue); 62 | } 63 | return -1; 64 | } 65 | 66 | private String findUri(String request) { 67 | Matcher matcher = URL_PATTERN.matcher(request); 68 | if (matcher.find()) { 69 | return matcher.group(1); 70 | } 71 | throw new IllegalArgumentException("Invalid request `" + request + "`: url not found!"); 72 | } 73 | 74 | @Override 75 | public String toString() { 76 | return "GetRequest{" + 77 | "rangeOffset=" + rangeOffset + 78 | ", partial=" + partial + 79 | ", uri='" + uri + '\'' + 80 | '}'; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/HttpProxyCache.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import android.text.TextUtils; 4 | 5 | import com.danikula.videocache.file.FileCache; 6 | 7 | import java.io.BufferedOutputStream; 8 | import java.io.IOException; 9 | import java.io.OutputStream; 10 | import java.net.Socket; 11 | import java.util.Locale; 12 | 13 | import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE; 14 | 15 | /** 16 | * {@link ProxyCache} that read http url and writes data to {@link Socket} 17 | * 18 | * @author Alexey Danilov (danikula@gmail.com). 19 | */ 20 | class HttpProxyCache extends ProxyCache { 21 | 22 | private static final float NO_CACHE_BARRIER = .2f; 23 | 24 | protected final HttpUrlSource source; 25 | protected final FileCache cache; 26 | protected CacheListener listener; 27 | 28 | public HttpProxyCache(HttpUrlSource source, FileCache cache) { 29 | super(source, cache); 30 | this.cache = cache; 31 | this.source = source; 32 | } 33 | 34 | public void registerCacheListener(CacheListener cacheListener) { 35 | this.listener = cacheListener; 36 | } 37 | 38 | public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException { 39 | OutputStream out = new BufferedOutputStream(socket.getOutputStream()); 40 | String responseHeaders = newResponseHeaders(request); 41 | out.write(responseHeaders.getBytes("UTF-8")); 42 | 43 | long offset = request.rangeOffset; 44 | if (isUseCache(request)) { 45 | responseWithCache(out, offset); 46 | } else { 47 | responseWithoutCache(out, offset); 48 | } 49 | } 50 | 51 | private boolean isUseCache(GetRequest request) throws ProxyCacheException { 52 | long sourceLength = source.length(); 53 | boolean sourceLengthKnown = sourceLength > 0; 54 | long cacheAvailable = cache.available(); 55 | // do not use cache for partial requests which too far from available cache. It seems user seek video. 56 | return !sourceLengthKnown || !request.partial || request.rangeOffset <= cacheAvailable + sourceLength * NO_CACHE_BARRIER; 57 | } 58 | 59 | private String newResponseHeaders(GetRequest request) throws IOException, ProxyCacheException { 60 | String mime = source.getMime(); 61 | boolean mimeKnown = !TextUtils.isEmpty(mime); 62 | long length = cache.isCompleted() ? cache.available() : source.length(); 63 | boolean lengthKnown = length >= 0; 64 | long contentLength = request.partial ? length - request.rangeOffset : length; 65 | boolean addRange = lengthKnown && request.partial; 66 | return new StringBuilder() 67 | .append(request.partial ? "HTTP/1.1 206 PARTIAL CONTENT\n" : "HTTP/1.1 200 OK\n") 68 | .append("Accept-Ranges: bytes\n") 69 | .append(lengthKnown ? format("Content-Length: %d\n", contentLength) : "") 70 | .append(addRange ? format("Content-Range: bytes %d-%d/%d\n", request.rangeOffset, length - 1, length) : "") 71 | .append(mimeKnown ? format("Content-Type: %s\n", mime) : "") 72 | .append("\n") // headers end 73 | .toString(); 74 | } 75 | 76 | private void responseWithCache(OutputStream out, long offset) throws ProxyCacheException, IOException { 77 | LogU.d("responseWithCache 请求带缓存数据"+offset); 78 | byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; 79 | int readBytes; 80 | while ((readBytes = read(buffer, offset, buffer.length)) != -1) { 81 | out.write(buffer, 0, readBytes); 82 | offset += readBytes; 83 | LogU.d("responseWithCache 返回给播放器"+offset); 84 | } 85 | out.flush(); 86 | } 87 | 88 | private void responseWithoutCache(OutputStream out, long offset) throws ProxyCacheException, IOException { 89 | LogU.d("responseWithCache 请求不带缓存数据"+offset); 90 | HttpUrlSource newSourceNoCache = new HttpUrlSource(this.source); 91 | try { 92 | newSourceNoCache.open((int) offset); 93 | byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; 94 | int readBytes; 95 | while ((readBytes = newSourceNoCache.read(buffer)) != -1) { 96 | LogU.d("responseWithCache 请求不带缓存数据"+offset+" readBytes "+readBytes); 97 | out.write(buffer, 0, readBytes); 98 | offset += readBytes; 99 | } 100 | out.flush(); 101 | } finally { 102 | newSourceNoCache.close(); 103 | } 104 | } 105 | 106 | private String format(String pattern, Object... args) { 107 | return String.format(Locale.US, pattern, args); 108 | } 109 | 110 | @Override 111 | protected void onCachePercentsAvailableChanged(int percents) { 112 | if (listener != null) { 113 | listener.onCacheAvailable(cache.file, source.getUrl(), percents); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/HttpProxyCacheServer.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import android.content.Context; 4 | import android.net.Uri; 5 | 6 | import com.danikula.videocache.file.DiskUsage; 7 | import com.danikula.videocache.file.FileNameGenerator; 8 | import com.danikula.videocache.file.Md5FileNameGenerator; 9 | import com.danikula.videocache.file.TotalCountLruDiskUsage; 10 | import com.danikula.videocache.file.TotalSizeLruDiskUsage; 11 | import com.danikula.videocache.headers.EmptyHeadersInjector; 12 | import com.danikula.videocache.headers.HeaderInjector; 13 | import com.danikula.videocache.sourcestorage.SourceInfoStorage; 14 | import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory; 15 | 16 | 17 | 18 | import java.io.File; 19 | import java.io.IOException; 20 | import java.net.InetAddress; 21 | import java.net.ServerSocket; 22 | import java.net.Socket; 23 | import java.net.SocketException; 24 | import java.util.Locale; 25 | import java.util.Map; 26 | import java.util.concurrent.ConcurrentHashMap; 27 | import java.util.concurrent.CountDownLatch; 28 | import java.util.concurrent.ExecutorService; 29 | import java.util.concurrent.Executors; 30 | 31 | import static com.danikula.videocache.HttpProxyPreLoader.preUrlPx; 32 | import static com.danikula.videocache.Preconditions.checkAllNotNull; 33 | import static com.danikula.videocache.Preconditions.checkNotNull; 34 | 35 | /** 36 | * Simple lightweight proxy server with file caching support that handles HTTP requests. 37 | * Typical usage: 38 | *

 39 |  * public onCreate(Bundle state) {
 40 |  *      super.onCreate(state);
 41 |  *
 42 |  *      HttpProxyCacheServer proxy = getProxy();
 43 |  *      String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
 44 |  *      videoView.setVideoPath(proxyUrl);
 45 |  * }
 46 |  *
 47 |  * private HttpProxyCacheServer getProxy() {
 48 |  * // should return single instance of HttpProxyCacheServer shared for whole app.
 49 |  * }
 50 |  * 
51 | * 52 | * @author Alexey Danilov (danikula@gmail.com). 53 | */ 54 | public class HttpProxyCacheServer { 55 | 56 | private static final String PROXY_HOST = "127.0.0.1"; 57 | 58 | private final Object clientsLock = new Object(); 59 | private final ExecutorService socketProcessor = Executors.newFixedThreadPool(8); 60 | private final Map clientsMap = new ConcurrentHashMap<>(); 61 | private final ServerSocket serverSocket; 62 | private final int port; 63 | private final Thread waitConnectionThread; 64 | private final Config config; 65 | private final Pinger pinger; 66 | private HttpProxyPreLoader preLoader; 67 | 68 | public HttpProxyCacheServer(Context context) { 69 | this(new Builder(context).buildConfig()); 70 | } 71 | 72 | private HttpProxyCacheServer(Config config) { 73 | preLoader = new HttpProxyPreLoader(); 74 | this.config = checkNotNull(config); 75 | try { 76 | InetAddress inetAddress = InetAddress.getByName(PROXY_HOST); 77 | this.serverSocket = new ServerSocket(0, 8, inetAddress); 78 | this.port = serverSocket.getLocalPort(); 79 | IgnoreHostProxySelector.install(PROXY_HOST, port); 80 | CountDownLatch startSignal = new CountDownLatch(1); 81 | this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal)); 82 | this.waitConnectionThread.start(); 83 | startSignal.await(); // freeze thread, wait for server starts 84 | this.pinger = new Pinger(PROXY_HOST, port); 85 | LogU.d("Proxy cache server started. Is it alive? " + isAlive()); 86 | } catch (IOException | InterruptedException e) { 87 | socketProcessor.shutdown(); 88 | throw new IllegalStateException("Error starting local proxy server", e); 89 | } 90 | } 91 | 92 | /** 93 | * Returns url that wrap original url and should be used for client (MediaPlayer, ExoPlayer, etc). 94 | *

95 | * If file for this url is fully cached (it means method {@link #isCached(String)} returns {@code true}) 96 | * then file:// uri to cached file will be returned. 97 | *

98 | * Calling this method has same effect as calling {@link #getProxyUrl(String, boolean)} with 2nd parameter set to {@code true}. 99 | * 100 | * @param url a url to file that should be cached. 101 | * @return a wrapped by proxy url if file is not fully cached or url pointed to cache file otherwise. 102 | */ 103 | public String getProxyUrl(String url) { 104 | return getProxyUrl(url, true); 105 | } 106 | 107 | /** 108 | * Returns url that wrap original url and should be used for client (MediaPlayer, ExoPlayer, etc). 109 | *

110 | * If parameter {@code allowCachedFileUri} is {@code true} and file for this url is fully cached 111 | * (it means method {@link #isCached(String)} returns {@code true}) then file:// uri to cached file will be returned. 112 | * 113 | * @param url a url to file that should be cached. 114 | * @param allowCachedFileUri {@code true} if allow to return file:// uri if url is fully cached 115 | * @return a wrapped by proxy url if file is not fully cached or url pointed to cache file otherwise (if {@code allowCachedFileUri} is {@code true}). 116 | */ 117 | public String getProxyUrl(String url, boolean allowCachedFileUri) { 118 | if (allowCachedFileUri && isCached(url)) { 119 | File cacheFile = getCacheFile(url); 120 | touchFileSafely(cacheFile); 121 | return Uri.fromFile(cacheFile).toString(); 122 | } 123 | return isAlive() ? appendToProxyUrl(url) : url; 124 | } 125 | 126 | public void registerCacheListener(CacheListener cacheListener, String url) { 127 | checkAllNotNull(cacheListener, url); 128 | synchronized (clientsLock) { 129 | try { 130 | getClients(url).registerCacheListener(cacheListener); 131 | } catch (ProxyCacheException e) { 132 | LogU.d("Error registering cache listener", e); 133 | } 134 | } 135 | } 136 | 137 | public void unregisterCacheListener(CacheListener cacheListener, String url) { 138 | checkAllNotNull(cacheListener, url); 139 | synchronized (clientsLock) { 140 | try { 141 | getClients(url).unregisterCacheListener(cacheListener); 142 | } catch (ProxyCacheException e) { 143 | LogU.d("Error registering cache listener", e); 144 | } 145 | } 146 | } 147 | 148 | public void unregisterCacheListener(CacheListener cacheListener) { 149 | checkNotNull(cacheListener); 150 | synchronized (clientsLock) { 151 | for (HttpProxyCacheServerClients clients : clientsMap.values()) { 152 | clients.unregisterCacheListener(cacheListener); 153 | } 154 | } 155 | } 156 | 157 | /** 158 | * Checks is cache contains fully cached file for particular url. 159 | * 160 | * @param url an url cache file will be checked for. 161 | * @return {@code true} if cache contains fully cached file for passed in parameters url. 162 | */ 163 | public boolean isCached(String url) { 164 | checkNotNull(url, "Url can't be null!"); 165 | return getCacheFile(url).exists(); 166 | } 167 | 168 | public void shutdown() { 169 | LogU.d("Shutdown proxy server"); 170 | 171 | shutdownClients(); 172 | 173 | config.sourceInfoStorage.release(); 174 | 175 | waitConnectionThread.interrupt(); 176 | try { 177 | if (!serverSocket.isClosed()) { 178 | serverSocket.close(); 179 | } 180 | } catch (IOException e) { 181 | onError(new ProxyCacheException("Error shutting down proxy server", e)); 182 | } 183 | } 184 | 185 | private boolean isAlive() { 186 | return pinger.ping(3, 70); // 70+140+280=max~500ms 187 | } 188 | 189 | private String appendToProxyUrl(String url) { 190 | return String.format(Locale.US, "http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url)); 191 | } 192 | 193 | private File getCacheFile(String url) { 194 | File cacheDir = config.cacheRoot; 195 | String fileName = config.fileNameGenerator.generate(url); 196 | return new File(cacheDir, fileName); 197 | } 198 | 199 | private void touchFileSafely(File cacheFile) { 200 | try { 201 | config.diskUsage.touch(cacheFile); 202 | } catch (IOException e) { 203 | LogU.e("Error touching file " + cacheFile, e); 204 | } 205 | } 206 | 207 | private void shutdownClients() { 208 | synchronized (clientsLock) { 209 | for (HttpProxyCacheServerClients clients : clientsMap.values()) { 210 | clients.shutdown(); 211 | } 212 | clientsMap.clear(); 213 | } 214 | } 215 | 216 | private void waitForRequest() { 217 | try { 218 | while (!Thread.currentThread().isInterrupted()) { 219 | Socket socket = serverSocket.accept(); 220 | LogU.d("Accept new socket " + socket); 221 | socketProcessor.submit(new SocketProcessorRunnable(socket)); 222 | } 223 | } catch (IOException e) { 224 | onError(new ProxyCacheException("Error during waiting connection", e)); 225 | } 226 | } 227 | 228 | private void processSocket(Socket socket) { 229 | try { 230 | GetRequest request = GetRequest.read(socket.getInputStream()); 231 | 232 | String originUrl = request.uri; 233 | String proxyUrl = originUrl; 234 | boolean isPreLoad = false; 235 | if (originUrl.contains(preUrlPx)) { 236 | //预下载 237 | isPreLoad = true; 238 | proxyUrl = originUrl.split(preUrlPx)[0]; 239 | } 240 | 241 | if (isPreLoad) { 242 | request.isPreLoad = true; 243 | request.percentsPreLoad = Integer.parseInt(originUrl.split(preUrlPx)[1].split("_")[1]); 244 | } else { 245 | request.isPreLoad = false; 246 | if (preLoader.isLoading(originUrl)) { 247 | preLoader.stopLoad(originUrl); 248 | } 249 | } 250 | 251 | LogU.d("Request to cache proxy:" + request); 252 | String url = ProxyCacheUtils.decode(proxyUrl); 253 | 254 | if (pinger.isPingRequest(url)) { 255 | pinger.responseToPing(socket); 256 | } else { 257 | 258 | HttpProxyCacheServerClients clients = getClients(url); 259 | clients.processRequest(request, socket); 260 | } 261 | } catch (SocketException e) { 262 | // There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458 263 | // So just to prevent log flooding don't log stacktrace 264 | LogU.d("Closing socket… Socket is closed by client."); 265 | } catch (ProxyCacheException | IOException e) { 266 | onError(new ProxyCacheException("Error processing request", e)); 267 | } finally { 268 | releaseSocket(socket); 269 | LogU.d("Opened connections: " + getClientsCount()); 270 | } 271 | } 272 | 273 | private HttpProxyCacheServerClients getClients(String url) throws ProxyCacheException { 274 | synchronized (clientsLock) { 275 | HttpProxyCacheServerClients clients = clientsMap.get(url); 276 | if (clients == null) { 277 | clients = new HttpProxyCacheServerClients(url, config); 278 | clientsMap.put(url, clients); 279 | } 280 | return clients; 281 | } 282 | } 283 | 284 | 285 | public boolean preLoad(final String url, final int percentsPreLoad) { 286 | final String proxyUrlOrigin = getProxyUrl(url, false); 287 | final String proxyUrl = proxyUrlOrigin + preUrlPx + "_" + percentsPreLoad; 288 | 289 | 290 | if (preLoader.isLoading(proxyUrl)) { 291 | return false; 292 | } else { 293 | // preLoader.startLoad(proxyUrl, percentsPreLoad, Math.abs(21474836)); 294 | preLoader.cachedThreadPool.execute(new Runnable() { 295 | @Override 296 | public void run() { 297 | HttpProxyCacheServerClients clients = null; 298 | try { 299 | clients = getClients(url); 300 | final HttpProxyCache cacheProxy = clients.startProcessRequest(); 301 | if (!cacheProxy.cache.isCompleted()) { 302 | long length = cacheProxy.source.length(); 303 | long cacheLen = cacheProxy.cache.available(); 304 | LogU.d("预加载文件大小" + length+" 本地缓存大小 "+cacheLen+ " "+(cacheLen < Math.abs(length) * (percentsPreLoad / 100.0))); 305 | if (cacheLen < Math.abs(length) * (percentsPreLoad / 100.0)) { 306 | preLoader.startLoad(proxyUrl, percentsPreLoad, Math.abs(length)); 307 | } 308 | } 309 | } catch (ProxyCacheException e) { 310 | e.printStackTrace(); 311 | } 312 | } 313 | }); 314 | 315 | } 316 | return true; 317 | } 318 | 319 | private boolean isLoading(String url) { 320 | HttpProxyCacheServerClients clients = clientsMap.get(url); 321 | return clients == null; 322 | } 323 | 324 | private int getClientsCount() { 325 | synchronized (clientsLock) { 326 | int count = 0; 327 | for (HttpProxyCacheServerClients clients : clientsMap.values()) { 328 | count += clients.getClientsCount(); 329 | } 330 | return count; 331 | } 332 | } 333 | 334 | private void releaseSocket(Socket socket) { 335 | closeSocketInput(socket); 336 | closeSocketOutput(socket); 337 | closeSocket(socket); 338 | } 339 | 340 | private void closeSocketInput(Socket socket) { 341 | try { 342 | if (!socket.isInputShutdown()) { 343 | socket.shutdownInput(); 344 | } 345 | } catch (SocketException e) { 346 | // There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458 347 | // So just to prevent log flooding don't log stacktrace 348 | LogU.d("Releasing input stream… Socket is closed by client."); 349 | } catch (IOException e) { 350 | onError(new ProxyCacheException("Error closing socket input stream", e)); 351 | } 352 | } 353 | 354 | private void closeSocketOutput(Socket socket) { 355 | try { 356 | if (!socket.isOutputShutdown()) { 357 | socket.shutdownOutput(); 358 | } 359 | } catch (IOException e) { 360 | LogU.d("Failed to close socket on proxy side: {}. It seems client have already closed connection.", e); 361 | } 362 | } 363 | 364 | private void closeSocket(Socket socket) { 365 | try { 366 | if (!socket.isClosed()) { 367 | socket.close(); 368 | } 369 | } catch (IOException e) { 370 | onError(new ProxyCacheException("Error closing socket", e)); 371 | } 372 | } 373 | 374 | private void onError(Throwable e) { 375 | LogU.e("HttpProxyCacheServer error", e); 376 | } 377 | 378 | private final class WaitRequestsRunnable implements Runnable { 379 | 380 | private final CountDownLatch startSignal; 381 | 382 | public WaitRequestsRunnable(CountDownLatch startSignal) { 383 | this.startSignal = startSignal; 384 | } 385 | 386 | @Override 387 | public void run() { 388 | startSignal.countDown(); 389 | waitForRequest(); 390 | } 391 | } 392 | 393 | private final class SocketProcessorRunnable implements Runnable { 394 | 395 | private final Socket socket; 396 | 397 | public SocketProcessorRunnable(Socket socket) { 398 | this.socket = socket; 399 | } 400 | 401 | @Override 402 | public void run() { 403 | processSocket(socket); 404 | } 405 | } 406 | 407 | /** 408 | * Builder for {@link HttpProxyCacheServer}. 409 | */ 410 | public static final class Builder { 411 | 412 | private static final long DEFAULT_MAX_SIZE = 512 * 1024 * 1024; 413 | 414 | private File cacheRoot; 415 | private FileNameGenerator fileNameGenerator; 416 | private DiskUsage diskUsage; 417 | private SourceInfoStorage sourceInfoStorage; 418 | private HeaderInjector headerInjector; 419 | 420 | public Builder(Context context) { 421 | this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context); 422 | this.cacheRoot = StorageUtils.getIndividualCacheDirectory(context); 423 | this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE); 424 | this.fileNameGenerator = new Md5FileNameGenerator(); 425 | this.headerInjector = new EmptyHeadersInjector(); 426 | } 427 | 428 | /** 429 | * Overrides default cache folder to be used for caching files. 430 | *

431 | * By default AndroidVideoCache uses 432 | * '/Android/data/[app_package_name]/cache/video-cache/' if card is mounted and app has appropriate permission 433 | * or 'video-cache' subdirectory in default application's cache directory otherwise. 434 | *

435 | * Note directory must be used only for AndroidVideoCache files. 436 | * 437 | * @param file a cache directory, can't be null. 438 | * @return a builder. 439 | */ 440 | public Builder cacheDirectory(File file) { 441 | this.cacheRoot = checkNotNull(file); 442 | return this; 443 | } 444 | 445 | /** 446 | * Overrides default cache file name generator {@link Md5FileNameGenerator} . 447 | * 448 | * @param fileNameGenerator a new file name generator. 449 | * @return a builder. 450 | */ 451 | public Builder fileNameGenerator(FileNameGenerator fileNameGenerator) { 452 | this.fileNameGenerator = checkNotNull(fileNameGenerator); 453 | return this; 454 | } 455 | 456 | /** 457 | * Sets max cache size in bytes. 458 | *

459 | * All files that exceeds limit will be deleted using LRU strategy. 460 | * Default value is 512 Mb. 461 | *

462 | * Note this method overrides result of calling {@link #maxCacheFilesCount(int)} 463 | * 464 | * @param maxSize max cache size in bytes. 465 | * @return a builder. 466 | */ 467 | public Builder maxCacheSize(long maxSize) { 468 | this.diskUsage = new TotalSizeLruDiskUsage(maxSize); 469 | return this; 470 | } 471 | 472 | /** 473 | * Sets max cache files count. 474 | * All files that exceeds limit will be deleted using LRU strategy. 475 | * Note this method overrides result of calling {@link #maxCacheSize(long)} 476 | * 477 | * @param count max cache files count. 478 | * @return a builder. 479 | */ 480 | public Builder maxCacheFilesCount(int count) { 481 | this.diskUsage = new TotalCountLruDiskUsage(count); 482 | return this; 483 | } 484 | 485 | /** 486 | * Set custom DiskUsage logic for handling when to keep or clean cache. 487 | * 488 | * @param diskUsage a disk usage strategy, cant be {@code null}. 489 | * @return a builder. 490 | */ 491 | public Builder diskUsage(DiskUsage diskUsage) { 492 | this.diskUsage = checkNotNull(diskUsage); 493 | return this; 494 | } 495 | 496 | /** 497 | * Add headers along the request to the server 498 | * 499 | * @param headerInjector to inject header base on url 500 | * @return a builder 501 | */ 502 | public Builder headerInjector(HeaderInjector headerInjector) { 503 | this.headerInjector = checkNotNull(headerInjector); 504 | return this; 505 | } 506 | 507 | /** 508 | * Builds new instance of {@link HttpProxyCacheServer}. 509 | * 510 | * @return proxy cache. Only single instance should be used across whole app. 511 | */ 512 | public HttpProxyCacheServer build() { 513 | Config config = buildConfig(); 514 | return new HttpProxyCacheServer(config); 515 | } 516 | 517 | private Config buildConfig() { 518 | return new Config(cacheRoot, fileNameGenerator, diskUsage, sourceInfoStorage, headerInjector); 519 | } 520 | 521 | } 522 | } 523 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/HttpProxyCacheServerClients.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | import android.os.Message; 6 | 7 | import com.danikula.videocache.file.FileCache; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.net.Socket; 12 | import java.util.List; 13 | import java.util.concurrent.CopyOnWriteArrayList; 14 | import java.util.concurrent.atomic.AtomicInteger; 15 | 16 | import static com.danikula.videocache.Preconditions.checkNotNull; 17 | 18 | /** 19 | * Client for {@link HttpProxyCacheServer} 20 | * 21 | * @author Alexey Danilov (danikula@gmail.com). 22 | */ 23 | final class HttpProxyCacheServerClients { 24 | 25 | private final AtomicInteger clientsCount = new AtomicInteger(0); 26 | private final String url; 27 | private volatile HttpProxyCache proxyCache; 28 | private final List listeners = new CopyOnWriteArrayList<>(); 29 | private final CacheListener uiCacheListener; 30 | private final Config config; 31 | 32 | 33 | public HttpProxyCacheServerClients(String url, Config config) { 34 | this.url = checkNotNull(url); 35 | this.config = checkNotNull(config); 36 | this.uiCacheListener = new UiListenerHandler(url, listeners); 37 | } 38 | 39 | public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException { 40 | 41 | startProcessRequest(); 42 | proxyCache.isPreLoad = request.isPreLoad; 43 | proxyCache.setPercentsPreLoad( request.percentsPreLoad); 44 | try { 45 | clientsCount.incrementAndGet(); 46 | proxyCache.processRequest(request, socket); 47 | } finally { 48 | finishProcessRequest(); 49 | } 50 | } 51 | 52 | protected synchronized HttpProxyCache startProcessRequest() throws ProxyCacheException { 53 | proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache; 54 | return proxyCache; 55 | } 56 | 57 | private synchronized void finishProcessRequest() { 58 | if (clientsCount.decrementAndGet() <= 0) { 59 | proxyCache.shutdown(); 60 | proxyCache = null; 61 | } 62 | } 63 | 64 | public void registerCacheListener(CacheListener cacheListener) { 65 | listeners.add(cacheListener); 66 | } 67 | 68 | public void unregisterCacheListener(CacheListener cacheListener) { 69 | listeners.remove(cacheListener); 70 | } 71 | 72 | public void shutdown() { 73 | listeners.clear(); 74 | if (proxyCache != null) { 75 | proxyCache.registerCacheListener(null); 76 | proxyCache.shutdown(); 77 | proxyCache = null; 78 | } 79 | clientsCount.set(0); 80 | } 81 | 82 | public int getClientsCount() { 83 | return clientsCount.get(); 84 | } 85 | 86 | private HttpProxyCache newHttpProxyCache() throws ProxyCacheException { 87 | HttpUrlSource source = new HttpUrlSource(url, config.sourceInfoStorage, config.headerInjector); 88 | FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage); 89 | HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache); 90 | httpProxyCache.registerCacheListener(uiCacheListener); 91 | return httpProxyCache; 92 | } 93 | 94 | private static final class UiListenerHandler extends Handler implements CacheListener { 95 | 96 | private final String url; 97 | private final List listeners; 98 | 99 | public UiListenerHandler(String url, List listeners) { 100 | super(Looper.getMainLooper()); 101 | this.url = url; 102 | this.listeners = listeners; 103 | } 104 | 105 | @Override 106 | public void onCacheAvailable(File file, String url, int percentsAvailable) { 107 | Message message = obtainMessage(); 108 | message.arg1 = percentsAvailable; 109 | message.obj = file; 110 | sendMessage(message); 111 | } 112 | 113 | @Override 114 | public void handleMessage(Message msg) { 115 | for (CacheListener cacheListener : listeners) { 116 | cacheListener.onCacheAvailable((File) msg.obj, url, msg.arg1); 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/HttpProxyPreLoader.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.concurrent.ExecutorService; 8 | import java.util.concurrent.Executors; 9 | 10 | import okhttp3.Request; 11 | import okhttp3.Response; 12 | 13 | public class HttpProxyPreLoader { 14 | 15 | private static String TAG = "HttpProxyPreLoader"; 16 | protected ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); 17 | static final String preUrlPx = "_HttpProxyPre"; 18 | private Map runnableMap = new HashMap<>(); 19 | 20 | public boolean isLoading(String url) { 21 | return runnableMap.get(url) != null; 22 | } 23 | 24 | public void startLoad(String url, int percentsPreLoad, long totalLen) { 25 | LogU.d("开始预下载 total " + url); 26 | PreRunnable runnable = new PreRunnable(url, percentsPreLoad,totalLen); 27 | runnableMap.put(url, runnable); 28 | cachedThreadPool.execute(runnable); 29 | } 30 | 31 | public int getPercentsPreLoad(String url) { 32 | PreRunnable runnable = runnableMap.get(url); 33 | if (runnable == null) { 34 | return 0; 35 | } else { 36 | return runnable.percentsPreLoad; 37 | } 38 | } 39 | 40 | public void stopLoad(String url) { 41 | PreRunnable runnable = runnableMap.get(url); 42 | if (runnable != null) { 43 | runnable.stop = true; 44 | LogU.d("停止预下载 total " + url); 45 | } 46 | } 47 | 48 | 49 | class PreRunnable implements Runnable { 50 | 51 | private String url; 52 | private int percentsPreLoad; 53 | 54 | protected boolean stop = false; 55 | private long len; 56 | 57 | public PreRunnable(String url, int percentsPreLoad, Long total) { 58 | this.url = url; 59 | this.len = total; 60 | this.percentsPreLoad = percentsPreLoad; 61 | } 62 | 63 | @Override 64 | public void run() { 65 | final Request request = new Request.Builder() 66 | .url(url) 67 | .head() 68 | .build(); 69 | 70 | try { 71 | // Response response = OkManager.getInstance().client.newCall(request).execute(); 72 | // long length = Long.parseLong(response.header("content-length"));//获取文件长度 73 | // long targetLen = length * (percentsPreLoad / 100L); 74 | int targetLen = (int) (len * (percentsPreLoad / 100.0)); 75 | ;//length * (percentsPreLoad / 100L); 76 | Request requestLoad = new Request.Builder() 77 | .url(url) 78 | .addHeader("Range", String.format("bytes=%d-%d", 0, targetLen)) 79 | .build(); 80 | 81 | Response responseLoad = OkManager.getInstance().client.newCall(requestLoad).execute(); 82 | InputStream inputStream = responseLoad.body().byteStream();//获取流 83 | 84 | 85 | final long M = 1024; 86 | byte[] bytes = new byte[(int) M]; 87 | long seek = 0; 88 | long total = 0; 89 | for (; ; ) { 90 | int readCount = inputStream.read(bytes); 91 | total += readCount; 92 | seek += readCount; 93 | LogU.d("预下载客户端 total " + total+" 应该下载 "+targetLen +" readCount " +readCount); 94 | if (readCount == 0) { 95 | continue; 96 | } 97 | if (total > targetLen) { 98 | break; 99 | } 100 | if (readCount == -1) { 101 | inputStream.close(); 102 | break; 103 | } 104 | if (stop) { 105 | inputStream.close(); 106 | break; 107 | } 108 | 109 | 110 | 111 | } 112 | 113 | responseLoad.close(); 114 | 115 | } catch (IOException e) { 116 | e.printStackTrace(); 117 | } finally { 118 | runnableMap.remove(url); 119 | } 120 | } 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/HttpUrlSource.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import android.text.TextUtils; 4 | 5 | import com.danikula.videocache.headers.EmptyHeadersInjector; 6 | import com.danikula.videocache.headers.HeaderInjector; 7 | import com.danikula.videocache.sourcestorage.SourceInfoStorage; 8 | import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory; 9 | 10 | 11 | import java.io.BufferedInputStream; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.io.InterruptedIOException; 15 | import java.net.HttpURLConnection; 16 | import java.net.URL; 17 | import java.util.Map; 18 | 19 | import static com.danikula.videocache.Preconditions.checkNotNull; 20 | import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE; 21 | import static java.net.HttpURLConnection.HTTP_MOVED_PERM; 22 | import static java.net.HttpURLConnection.HTTP_MOVED_TEMP; 23 | import static java.net.HttpURLConnection.HTTP_OK; 24 | import static java.net.HttpURLConnection.HTTP_PARTIAL; 25 | import static java.net.HttpURLConnection.HTTP_SEE_OTHER; 26 | 27 | /** 28 | * {@link Source} that uses http resource as source for {@link ProxyCache}. 29 | * 30 | * @author Alexey Danilov (danikula@gmail.com). 31 | */ 32 | public class HttpUrlSource implements Source { 33 | 34 | 35 | 36 | private static final int MAX_REDIRECTS = 5; 37 | private final SourceInfoStorage sourceInfoStorage; 38 | private final HeaderInjector headerInjector; 39 | private SourceInfo sourceInfo; 40 | private HttpURLConnection connection; 41 | private InputStream inputStream; 42 | 43 | public HttpUrlSource(String url) { 44 | this(url, SourceInfoStorageFactory.newEmptySourceInfoStorage()); 45 | } 46 | 47 | public HttpUrlSource(String url, SourceInfoStorage sourceInfoStorage) { 48 | this(url, sourceInfoStorage, new EmptyHeadersInjector()); 49 | } 50 | 51 | public HttpUrlSource(String url, SourceInfoStorage sourceInfoStorage, HeaderInjector headerInjector) { 52 | this.sourceInfoStorage = checkNotNull(sourceInfoStorage); 53 | this.headerInjector = checkNotNull(headerInjector); 54 | SourceInfo sourceInfo = sourceInfoStorage.get(url); 55 | this.sourceInfo = sourceInfo != null ? sourceInfo : 56 | new SourceInfo(url, Integer.MIN_VALUE, ProxyCacheUtils.getSupposablyMime(url)); 57 | } 58 | 59 | public HttpUrlSource(HttpUrlSource source) { 60 | this.sourceInfo = source.sourceInfo; 61 | this.sourceInfoStorage = source.sourceInfoStorage; 62 | this.headerInjector = source.headerInjector; 63 | } 64 | 65 | @Override 66 | public synchronized long length() throws ProxyCacheException { 67 | if (sourceInfo.length == Integer.MIN_VALUE) { 68 | fetchContentInfo(); 69 | } 70 | return sourceInfo.length; 71 | } 72 | 73 | @Override 74 | public void open(long offset) throws ProxyCacheException { 75 | try { 76 | connection = openConnection(offset, -1); 77 | String mime = connection.getContentType(); 78 | inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE); 79 | long length = readSourceAvailableBytes(connection, offset, connection.getResponseCode()); 80 | this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime); 81 | this.sourceInfoStorage.put(sourceInfo.url, sourceInfo); 82 | } catch (IOException e) { 83 | throw new ProxyCacheException("Error opening connection for " + sourceInfo.url + " with offset " + offset, e); 84 | } 85 | } 86 | 87 | private long readSourceAvailableBytes(HttpURLConnection connection, long offset, int responseCode) throws IOException { 88 | long contentLength = getContentLength(connection); 89 | return responseCode == HTTP_OK ? contentLength 90 | : responseCode == HTTP_PARTIAL ? contentLength + offset : sourceInfo.length; 91 | } 92 | 93 | private long getContentLength(HttpURLConnection connection) { 94 | String contentLengthValue = connection.getHeaderField("Content-Length"); 95 | return contentLengthValue == null ? -1 : Long.parseLong(contentLengthValue); 96 | } 97 | 98 | @Override 99 | public void close() throws ProxyCacheException { 100 | if (connection != null) { 101 | try { 102 | connection.disconnect(); 103 | } catch (NullPointerException | IllegalArgumentException e) { 104 | String message = "Wait... but why? WTF!? " + 105 | "Really shouldn't happen any more after fixing https://github.com/danikula/AndroidVideoCache/issues/43. " + 106 | "If you read it on your device log, please, notify me danikula@gmail.com or create issue here " + 107 | "https://github.com/danikula/AndroidVideoCache/issues."; 108 | throw new RuntimeException(message, e); 109 | } catch (ArrayIndexOutOfBoundsException e) { 110 | LogU.e("Error closing connection correctly. Should happen only on Android L. " + 111 | "If anybody know how to fix it, please visit https://github.com/danikula/AndroidVideoCache/issues/88. " + 112 | "Until good solution is not know, just ignore this issue :(",e); 113 | } 114 | } 115 | } 116 | 117 | @Override 118 | public int read(byte[] buffer) throws ProxyCacheException { 119 | if (inputStream == null) { 120 | throw new ProxyCacheException("Error reading data from " + sourceInfo.url + ": connection is absent!"); 121 | } 122 | try { 123 | return inputStream.read(buffer, 0, buffer.length); 124 | } catch (InterruptedIOException e) { 125 | throw new InterruptedProxyCacheException("Reading source " + sourceInfo.url + " is interrupted", e); 126 | } catch (IOException e) { 127 | throw new ProxyCacheException("Error reading data from " + sourceInfo.url, e); 128 | } 129 | } 130 | 131 | private void fetchContentInfo() throws ProxyCacheException { 132 | LogU.d("Read content info from " + sourceInfo.url); 133 | HttpURLConnection urlConnection = null; 134 | InputStream inputStream = null; 135 | try { 136 | urlConnection = openConnection(0, 10000); 137 | long length = getContentLength(urlConnection); 138 | String mime = urlConnection.getContentType(); 139 | inputStream = urlConnection.getInputStream(); 140 | this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime); 141 | this.sourceInfoStorage.put(sourceInfo.url, sourceInfo); 142 | LogU.d("Source info fetched: " + sourceInfo); 143 | } catch (IOException e) { 144 | LogU.e("Error fetching info from " + sourceInfo.url, e); 145 | } finally { 146 | ProxyCacheUtils.close(inputStream); 147 | if (urlConnection != null) { 148 | urlConnection.disconnect(); 149 | } 150 | } 151 | } 152 | 153 | private HttpURLConnection openConnection(long offset, int timeout) throws IOException, ProxyCacheException { 154 | HttpURLConnection connection; 155 | boolean redirected; 156 | int redirectCount = 0; 157 | String url = this.sourceInfo.url; 158 | do { 159 | LogU.d("Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url); 160 | connection = (HttpURLConnection) new URL(url).openConnection(); 161 | injectCustomHeaders(connection, url); 162 | if (offset > 0) { 163 | connection.setRequestProperty("Range", "bytes=" + offset + "-"); 164 | } 165 | if (timeout > 0) { 166 | connection.setConnectTimeout(timeout); 167 | connection.setReadTimeout(timeout); 168 | } 169 | int code = connection.getResponseCode(); 170 | redirected = code == HTTP_MOVED_PERM || code == HTTP_MOVED_TEMP || code == HTTP_SEE_OTHER; 171 | if (redirected) { 172 | url = connection.getHeaderField("Location"); 173 | redirectCount++; 174 | connection.disconnect(); 175 | } 176 | if (redirectCount > MAX_REDIRECTS) { 177 | throw new ProxyCacheException("Too many redirects: " + redirectCount); 178 | } 179 | } while (redirected); 180 | return connection; 181 | } 182 | 183 | private void injectCustomHeaders(HttpURLConnection connection, String url) { 184 | Map extraHeaders = headerInjector.addHeaders(url); 185 | for (Map.Entry header : extraHeaders.entrySet()) { 186 | connection.setRequestProperty(header.getKey(), header.getValue()); 187 | } 188 | } 189 | 190 | public synchronized String getMime() throws ProxyCacheException { 191 | if (TextUtils.isEmpty(sourceInfo.mime)) { 192 | fetchContentInfo(); 193 | } 194 | return sourceInfo.mime; 195 | } 196 | 197 | public String getUrl() { 198 | return sourceInfo.url; 199 | } 200 | 201 | @Override 202 | public String toString() { 203 | return "HttpUrlSource{sourceInfo='" + sourceInfo + "}"; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/IgnoreHostProxySelector.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import java.io.IOException; 4 | import java.net.Proxy; 5 | import java.net.ProxySelector; 6 | import java.net.SocketAddress; 7 | import java.net.URI; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | import static com.danikula.videocache.Preconditions.checkNotNull; 12 | 13 | /** 14 | * {@link ProxySelector} that ignore system default proxies for concrete host. 15 | *

16 | * It is important to ignore system proxy for localhost connection. 17 | * 18 | * @author Alexey Danilov (danikula@gmail.com). 19 | */ 20 | class IgnoreHostProxySelector extends ProxySelector { 21 | 22 | private static final List NO_PROXY_LIST = Arrays.asList(Proxy.NO_PROXY); 23 | 24 | private final ProxySelector defaultProxySelector; 25 | private final String hostToIgnore; 26 | private final int portToIgnore; 27 | 28 | IgnoreHostProxySelector(ProxySelector defaultProxySelector, String hostToIgnore, int portToIgnore) { 29 | this.defaultProxySelector = checkNotNull(defaultProxySelector); 30 | this.hostToIgnore = checkNotNull(hostToIgnore); 31 | this.portToIgnore = portToIgnore; 32 | } 33 | 34 | static void install(String hostToIgnore, int portToIgnore) { 35 | ProxySelector defaultProxySelector = ProxySelector.getDefault(); 36 | ProxySelector ignoreHostProxySelector = new IgnoreHostProxySelector(defaultProxySelector, hostToIgnore, portToIgnore); 37 | ProxySelector.setDefault(ignoreHostProxySelector); 38 | } 39 | 40 | @Override 41 | public List select(URI uri) { 42 | boolean ignored = hostToIgnore.equals(uri.getHost()) && portToIgnore == uri.getPort(); 43 | return ignored ? NO_PROXY_LIST : defaultProxySelector.select(uri); 44 | } 45 | 46 | @Override 47 | public void connectFailed(URI uri, SocketAddress address, IOException failure) { 48 | defaultProxySelector.connectFailed(uri, address, failure); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/InterruptedProxyCacheException.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | /** 4 | * Indicates interruption error in work of {@link ProxyCache} fired by user. 5 | * 6 | * @author Alexey Danilov 7 | */ 8 | public class InterruptedProxyCacheException extends ProxyCacheException { 9 | 10 | public InterruptedProxyCacheException(String message) { 11 | super(message); 12 | } 13 | 14 | public InterruptedProxyCacheException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | 18 | public InterruptedProxyCacheException(Throwable cause) { 19 | super(cause); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/LogU.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import android.util.Log; 4 | 5 | public class LogU { 6 | 7 | public static void d(String msg){ 8 | Log.d("hapivideocache",msg); 9 | } 10 | public static void d(String msg,Throwable e){ 11 | Log.d("hapivideocache",msg+e.getMessage()); } 12 | 13 | public static void e(String msg,Throwable e){ 14 | Log.e("hapivideocache",msg+e.getMessage()); } 15 | public static void e(String msg){ 16 | Log.e("hapivideocache",msg); } 17 | } 18 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/OkManager.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import okhttp3.OkHttpClient; 4 | 5 | public class OkManager { 6 | 7 | protected OkHttpClient client; // = createOkHttpClient(); 8 | private OkManager(){ 9 | client = new OkHttpClient(); 10 | } 11 | public static OkManager getInstance(){ 12 | return Holder.ok; 13 | } 14 | private static class Holder{ 15 | public static OkManager ok = new OkManager(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/Pinger.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | 4 | 5 | import java.io.IOException; 6 | import java.io.OutputStream; 7 | import java.net.Proxy; 8 | import java.net.ProxySelector; 9 | import java.net.Socket; 10 | import java.net.URI; 11 | import java.net.URISyntaxException; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | import java.util.Locale; 15 | import java.util.concurrent.Callable; 16 | import java.util.concurrent.ExecutionException; 17 | import java.util.concurrent.ExecutorService; 18 | import java.util.concurrent.Executors; 19 | import java.util.concurrent.Future; 20 | import java.util.concurrent.TimeoutException; 21 | 22 | import static com.danikula.videocache.Preconditions.checkArgument; 23 | import static com.danikula.videocache.Preconditions.checkNotNull; 24 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 25 | 26 | /** 27 | * Pings {@link HttpProxyCacheServer} to make sure it works. 28 | * 29 | * @author Alexey Danilov (danikula@gmail.com). 30 | */ 31 | 32 | class Pinger { 33 | 34 | private static final String PING_REQUEST = "ping"; 35 | private static final String PING_RESPONSE = "ping ok"; 36 | 37 | private final ExecutorService pingExecutor = Executors.newSingleThreadExecutor(); 38 | private final String host; 39 | private final int port; 40 | 41 | Pinger(String host, int port) { 42 | this.host = checkNotNull(host); 43 | this.port = port; 44 | } 45 | 46 | boolean ping(int maxAttempts, int startTimeout) { 47 | checkArgument(maxAttempts >= 1); 48 | checkArgument(startTimeout > 0); 49 | 50 | int timeout = startTimeout; 51 | int attempts = 0; 52 | while (attempts < maxAttempts) { 53 | try { 54 | Future pingFuture = pingExecutor.submit(new PingCallable()); 55 | boolean pinged = pingFuture.get(timeout, MILLISECONDS); 56 | if (pinged) { 57 | return true; 58 | } 59 | } catch (TimeoutException e) { 60 | LogU.d("Error pinging server (attempt: " + attempts + ", timeout: " + timeout + "). "); 61 | } catch (InterruptedException | ExecutionException e) { 62 | LogU.e("Error pinging server due to unexpected error", e); 63 | } 64 | attempts++; 65 | timeout *= 2; 66 | } 67 | String error = String.format(Locale.US, "Error pinging server (attempts: %d, max timeout: %d). " + 68 | "If you see this message, please, report at https://github.com/danikula/AndroidVideoCache/issues/134. " + 69 | "Default proxies are: %s" 70 | , attempts, timeout / 2, getDefaultProxies()); 71 | LogU.e(error, new ProxyCacheException(error)); 72 | return false; 73 | } 74 | 75 | private List getDefaultProxies() { 76 | try { 77 | ProxySelector defaultProxySelector = ProxySelector.getDefault(); 78 | return defaultProxySelector.select(new URI(getPingUrl())); 79 | } catch (URISyntaxException e) { 80 | throw new IllegalStateException(e); 81 | } 82 | } 83 | 84 | boolean isPingRequest(String request) { 85 | return PING_REQUEST.equals(request); 86 | } 87 | 88 | void responseToPing(Socket socket) throws IOException { 89 | OutputStream out = socket.getOutputStream(); 90 | out.write("HTTP/1.1 200 OK\n\n".getBytes()); 91 | out.write(PING_RESPONSE.getBytes()); 92 | } 93 | 94 | private boolean pingServer() throws ProxyCacheException { 95 | String pingUrl = getPingUrl(); 96 | HttpUrlSource source = new HttpUrlSource(pingUrl); 97 | try { 98 | byte[] expectedResponse = PING_RESPONSE.getBytes(); 99 | source.open(0); 100 | byte[] response = new byte[expectedResponse.length]; 101 | source.read(response); 102 | boolean pingOk = Arrays.equals(expectedResponse, response); 103 | LogU.d("Ping response: `" + new String(response) + "`, pinged? " + pingOk); 104 | return pingOk; 105 | } catch (ProxyCacheException e) { 106 | LogU.e("Error reading ping response", e); 107 | return false; 108 | } finally { 109 | source.close(); 110 | } 111 | } 112 | 113 | private String getPingUrl() { 114 | return String.format(Locale.US, "http://%s:%d/%s", host, port, PING_REQUEST); 115 | } 116 | 117 | private class PingCallable implements Callable { 118 | 119 | @Override 120 | public Boolean call() throws Exception { 121 | return pingServer(); 122 | } 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/Preconditions.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | public final class Preconditions { 4 | 5 | public static T checkNotNull(T reference) { 6 | if (reference == null) { 7 | throw new NullPointerException(); 8 | } 9 | return reference; 10 | } 11 | 12 | public static void checkAllNotNull(Object... references) { 13 | for (Object reference : references) { 14 | if (reference == null) { 15 | throw new NullPointerException(); 16 | } 17 | } 18 | } 19 | 20 | public static T checkNotNull(T reference, String errorMessage) { 21 | if (reference == null) { 22 | throw new NullPointerException(errorMessage); 23 | } 24 | return reference; 25 | } 26 | 27 | static void checkArgument(boolean expression) { 28 | if (!expression) { 29 | throw new IllegalArgumentException(); 30 | } 31 | } 32 | 33 | static void checkArgument(boolean expression, String errorMessage) { 34 | if (!expression) { 35 | throw new IllegalArgumentException(errorMessage); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/ProxyCache.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | 4 | 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | 7 | import static com.danikula.videocache.Preconditions.checkNotNull; 8 | 9 | /** 10 | * Proxy for {@link Source} with caching support ({@link Cache}). 11 | *

12 | * Can be used only for sources with persistent data (that doesn't change with time). 13 | * Method {@link #read(byte[], long, int)} will be blocked while fetching data from source. 14 | * Useful for streaming something with caching e.g. streaming video/audio etc. 15 | * 16 | * @author Alexey Danilov (danikula@gmail.com). 17 | */ 18 | class ProxyCache { 19 | 20 | private static final int MAX_READ_SOURCE_ATTEMPTS = 1; 21 | 22 | private final Source source; 23 | private final Cache cache; 24 | private final Object wc = new Object(); 25 | private final Object stopLock = new Object(); 26 | private final AtomicInteger readSourceErrorsCount; 27 | private volatile Thread sourceReaderThread; 28 | private volatile boolean stopped; 29 | private volatile int percentsAvailable = -1; 30 | 31 | 32 | /** 33 | * 预下载 34 | */ 35 | public boolean isPreLoad = false; 36 | /** 37 | * 预下载百分比 38 | */ 39 | private int percentsPreLoad = 4; 40 | 41 | public void setPercentsPreLoad(int percentsPreLoad){ 42 | this.percentsPreLoad=percentsPreLoad; 43 | } 44 | public ProxyCache(Source source, Cache cache) { 45 | this.source = checkNotNull(source); 46 | this.cache = checkNotNull(cache); 47 | this.readSourceErrorsCount = new AtomicInteger(); 48 | } 49 | 50 | public int read(byte[] buffer, long offset, int length) throws ProxyCacheException { 51 | ProxyCacheUtils.assertBuffer(buffer, offset, length); 52 | 53 | Boolean isCompleted = cache.isCompleted(); 54 | Long available = cache.available(); 55 | while (!isCompleted && available < (offset + length) && !stopped) { 56 | LogU.d("请求读写 isCompleted " + isCompleted + " available " + available + " (offset + length) " + (offset + length) + " length " + length + " stopped " + stopped + " 是否读好了 " + (!isCompleted && available < (offset + length) && !stopped)); 57 | 58 | readSourceAsync(); 59 | waitForSourceData(); 60 | checkReadSourceErrorsCount(); 61 | isCompleted = cache.isCompleted(); 62 | available = cache.available(); 63 | } 64 | int read = cache.read(buffer, offset, length); 65 | if (cache.isCompleted() && percentsAvailable != 100) { 66 | percentsAvailable = 100; 67 | onCachePercentsAvailableChanged(100); 68 | } 69 | LogU.d("请求读写 返回读写字节"); 70 | return read; 71 | } 72 | 73 | private void checkReadSourceErrorsCount() throws ProxyCacheException { 74 | int errorsCount = readSourceErrorsCount.get(); 75 | if (errorsCount >= MAX_READ_SOURCE_ATTEMPTS) { 76 | readSourceErrorsCount.set(0); 77 | throw new ProxyCacheException("Error reading source " + errorsCount + " times"); 78 | } 79 | } 80 | 81 | public void shutdown() { 82 | synchronized (stopLock) { 83 | LogU.d("Shutdown proxy for " + source); 84 | try { 85 | stopped = true; 86 | if (sourceReaderThread != null) { 87 | sourceReaderThread.interrupt(); 88 | } 89 | cache.close(); 90 | } catch (ProxyCacheException e) { 91 | onError(e); 92 | } 93 | } 94 | } 95 | 96 | private synchronized void readSourceAsync() throws ProxyCacheException { 97 | 98 | 99 | boolean readingInProgress = sourceReaderThread != null && sourceReaderThread.getState() != Thread.State.TERMINATED; 100 | if (!stopped && !cache.isCompleted() && !readingInProgress) { 101 | 102 | sourceReaderThread = new Thread(new SourceReaderRunnable(), "Source reader for " + source); 103 | sourceReaderThread.start(); 104 | } 105 | } 106 | 107 | private void waitForSourceData() throws ProxyCacheException { 108 | synchronized (wc) { 109 | try { 110 | wc.wait(1000); 111 | } catch (InterruptedException e) { 112 | throw new ProxyCacheException("Waiting source data is interrupted!", e); 113 | } 114 | } 115 | } 116 | 117 | private void notifyNewCacheDataAvailable(long cacheAvailable, long sourceAvailable) { 118 | onCacheAvailable(cacheAvailable, sourceAvailable); 119 | 120 | synchronized (wc) { 121 | wc.notifyAll(); 122 | } 123 | } 124 | 125 | protected void onCacheAvailable(long cacheAvailable, long sourceLength) { 126 | boolean zeroLengthSource = sourceLength == 0; 127 | int percents = zeroLengthSource ? 100 : (int) ((float) cacheAvailable / sourceLength * 100); 128 | boolean percentsChanged = percents != percentsAvailable; 129 | boolean sourceLengthKnown = sourceLength >= 0; 130 | if (sourceLengthKnown && percentsChanged) { 131 | onCachePercentsAvailableChanged(percents); 132 | } 133 | percentsAvailable = percents; 134 | } 135 | 136 | protected void onCachePercentsAvailableChanged(int percentsAvailable) { 137 | } 138 | 139 | private void readSource() { 140 | long sourceAvailable = -1; 141 | long offset = 0; 142 | try { 143 | offset = cache.available(); 144 | source.open(offset); 145 | sourceAvailable = source.length(); 146 | byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE]; 147 | int readBytes; 148 | boolean breakPre = true; 149 | while (breakPre && (readBytes = source.read(buffer)) != -1) { 150 | synchronized (stopLock) { 151 | if (isStopped()) { 152 | return; 153 | } 154 | cache.append(buffer, readBytes); 155 | } 156 | offset += readBytes; 157 | notifyNewCacheDataAvailable(offset, sourceAvailable); 158 | LogU.d("读取网络" + readBytes); 159 | LogU.d("offset " + offset); 160 | LogU.d("percentsAvailable " + percentsAvailable + " percentsPreLoad " + percentsPreLoad); 161 | if (isPreLoad) { 162 | if (percentsAvailable >= percentsPreLoad) { 163 | LogU.d("offset 超过了不缓存" + offset); 164 | breakPre = false; 165 | break; 166 | } 167 | } 168 | } 169 | tryComplete(); 170 | onSourceRead(); 171 | } catch (Throwable e) { 172 | readSourceErrorsCount.incrementAndGet(); 173 | LogU.d("读取网络出错"); 174 | onError(e); 175 | } finally { 176 | LogU.d("读取网络完成"); 177 | closeSource(); 178 | notifyNewCacheDataAvailable(offset, sourceAvailable); 179 | } 180 | } 181 | 182 | private void onSourceRead() { 183 | // guaranteed notify listeners after source read and cache completed 184 | percentsAvailable = 100; 185 | onCachePercentsAvailableChanged(percentsAvailable); 186 | } 187 | 188 | private void tryComplete() throws ProxyCacheException { 189 | synchronized (stopLock) { 190 | if (!isStopped() && cache.available() == source.length()) { 191 | cache.complete(); 192 | } 193 | } 194 | } 195 | 196 | private boolean isStopped() { 197 | return Thread.currentThread().isInterrupted() || stopped; 198 | } 199 | 200 | private void closeSource() { 201 | try { 202 | source.close(); 203 | } catch (ProxyCacheException e) { 204 | onError(new ProxyCacheException("Error closing source " + source, e)); 205 | } 206 | } 207 | 208 | protected final void onError(final Throwable e) { 209 | boolean interruption = e instanceof InterruptedProxyCacheException; 210 | if (interruption) { 211 | LogU.d("ProxyCache is interrupted"); 212 | } else { 213 | LogU.e("ProxyCache error", e); 214 | } 215 | } 216 | 217 | private class SourceReaderRunnable implements Runnable { 218 | 219 | @Override 220 | public void run() { 221 | readSource(); 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/ProxyCacheException.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | /** 4 | * Indicates any error in work of {@link ProxyCache}. 5 | * 6 | * @author Alexey Danilov 7 | */ 8 | public class ProxyCacheException extends Exception { 9 | 10 | private static final String LIBRARY_VERSION = ". Version: " + BuildConfig.VERSION_NAME; 11 | 12 | public ProxyCacheException(String message) { 13 | super(message + LIBRARY_VERSION); 14 | } 15 | 16 | public ProxyCacheException(String message, Throwable cause) { 17 | super(message + LIBRARY_VERSION, cause); 18 | } 19 | 20 | public ProxyCacheException(Throwable cause) { 21 | super("No explanation error" + LIBRARY_VERSION, cause); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/ProxyCacheUtils.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import android.text.TextUtils; 4 | import android.webkit.MimeTypeMap; 5 | 6 | 7 | 8 | import java.io.Closeable; 9 | import java.io.IOException; 10 | import java.io.UnsupportedEncodingException; 11 | import java.net.URLDecoder; 12 | import java.net.URLEncoder; 13 | import java.security.MessageDigest; 14 | import java.security.NoSuchAlgorithmException; 15 | import java.util.Arrays; 16 | 17 | import static com.danikula.videocache.Preconditions.checkArgument; 18 | import static com.danikula.videocache.Preconditions.checkNotNull; 19 | 20 | /** 21 | * Just simple utils. 22 | * 23 | * @author Alexey Danilov (danikula@gmail.com). 24 | */ 25 | public class ProxyCacheUtils { 26 | 27 | static final int DEFAULT_BUFFER_SIZE = 8 * 1024; 28 | static final int MAX_ARRAY_PREVIEW = 16; 29 | 30 | static String getSupposablyMime(String url) { 31 | MimeTypeMap mimes = MimeTypeMap.getSingleton(); 32 | String extension = MimeTypeMap.getFileExtensionFromUrl(url); 33 | return TextUtils.isEmpty(extension) ? null : mimes.getMimeTypeFromExtension(extension); 34 | } 35 | 36 | static void assertBuffer(byte[] buffer, long offset, int length) { 37 | checkNotNull(buffer, "Buffer must be not null!"); 38 | checkArgument(offset >= 0, "Data offset must be positive!"); 39 | checkArgument(length >= 0 && length <= buffer.length, "Length must be in range [0..buffer.length]"); 40 | } 41 | 42 | static String preview(byte[] data, int length) { 43 | int previewLength = Math.min(MAX_ARRAY_PREVIEW, Math.max(length, 0)); 44 | byte[] dataRange = Arrays.copyOfRange(data, 0, previewLength); 45 | String preview = Arrays.toString(dataRange); 46 | if (previewLength < length) { 47 | preview = preview.substring(0, preview.length() - 1) + ", ...]"; 48 | } 49 | return preview; 50 | } 51 | 52 | static String encode(String url) { 53 | try { 54 | return URLEncoder.encode(url, "utf-8"); 55 | } catch (UnsupportedEncodingException e) { 56 | throw new RuntimeException("Error encoding url", e); 57 | } 58 | } 59 | 60 | static String decode(String url) { 61 | try { 62 | return URLDecoder.decode(url, "utf-8"); 63 | } catch (UnsupportedEncodingException e) { 64 | throw new RuntimeException("Error decoding url", e); 65 | } 66 | } 67 | 68 | static void close(Closeable closeable) { 69 | if (closeable != null) { 70 | try { 71 | closeable.close(); 72 | } catch (IOException e) { 73 | LogU.e("Error closing resource", e); 74 | } 75 | } 76 | } 77 | 78 | public static String computeMD5(String string) { 79 | try { 80 | MessageDigest messageDigest = MessageDigest.getInstance("MD5"); 81 | byte[] digestBytes = messageDigest.digest(string.getBytes()); 82 | return bytesToHexString(digestBytes); 83 | } catch (NoSuchAlgorithmException e) { 84 | throw new IllegalStateException(e); 85 | } 86 | } 87 | 88 | private static String bytesToHexString(byte[] bytes) { 89 | StringBuffer sb = new StringBuffer(); 90 | for (byte b : bytes) { 91 | sb.append(String.format("%02x", b)); 92 | } 93 | return sb.toString(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/Source.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | /** 4 | * Source for proxy. 5 | * 6 | * @author Alexey Danilov (danikula@gmail.com). 7 | */ 8 | public interface Source { 9 | 10 | /** 11 | * Opens source. Source should be open before using {@link #read(byte[])} 12 | * 13 | * @param offset offset in bytes for source. 14 | * @throws ProxyCacheException if error occur while opening source. 15 | */ 16 | void open(long offset) throws ProxyCacheException; 17 | 18 | /** 19 | * Returns length bytes or negative value if length is unknown. 20 | * 21 | * @return bytes length 22 | * @throws ProxyCacheException if error occur while fetching source data. 23 | */ 24 | long length() throws ProxyCacheException; 25 | 26 | /** 27 | * Read data to byte buffer from source with current offset. 28 | * 29 | * @param buffer a buffer to be used for reading data. 30 | * @return a count of read bytes 31 | * @throws ProxyCacheException if error occur while reading source. 32 | */ 33 | int read(byte[] buffer) throws ProxyCacheException; 34 | 35 | /** 36 | * Closes source and release resources. Every opened source should be closed. 37 | * 38 | * @throws ProxyCacheException if error occur while closing source. 39 | */ 40 | void close() throws ProxyCacheException; 41 | } 42 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/SourceInfo.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | /** 4 | * Stores source's info. 5 | * 6 | * @author Alexey Danilov (danikula@gmail.com). 7 | */ 8 | public class SourceInfo { 9 | 10 | public final String url; 11 | public final long length; 12 | public final String mime; 13 | 14 | public SourceInfo(String url, long length, String mime) { 15 | this.url = url; 16 | this.length = length; 17 | this.mime = mime; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return "SourceInfo{" + 23 | "url='" + url + '\'' + 24 | ", length=" + length + 25 | ", mime='" + mime + '\'' + 26 | '}'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/StorageUtils.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import android.content.Context; 4 | import android.os.Environment; 5 | 6 | 7 | import java.io.File; 8 | 9 | import static android.os.Environment.MEDIA_MOUNTED; 10 | 11 | /** 12 | * Provides application storage paths 13 | *

14 | * See https://github.com/nostra13/Android-Universal-Image-Loader 15 | * 16 | * @author Sergey Tarasevich (nostra13[at]gmail[dot]com) 17 | * @since 1.0.0 18 | */ 19 | final class StorageUtils { 20 | 21 | 22 | private static final String INDIVIDUAL_DIR_NAME = "video-cache"; 23 | 24 | /** 25 | * Returns individual application cache directory (for only video caching from Proxy). Cache directory will be 26 | * created on SD card ("/Android/data/[app_package_name]/cache/video-cache") if card is mounted . 27 | * Else - Android defines cache directory on device's file system. 28 | * 29 | * @param context Application context 30 | * @return Cache {@link File directory} 31 | */ 32 | public static File getIndividualCacheDirectory(Context context) { 33 | File cacheDir = getCacheDirectory(context, true); 34 | return new File(cacheDir, INDIVIDUAL_DIR_NAME); 35 | } 36 | 37 | /** 38 | * Returns application cache directory. Cache directory will be created on SD card 39 | * ("/Android/data/[app_package_name]/cache") (if card is mounted and app has appropriate permission) or 40 | * on device's file system depending incoming parameters. 41 | * 42 | * @param context Application context 43 | * @param preferExternal Whether prefer external location for cache 44 | * @return Cache {@link File directory}.
45 | * NOTE: Can be null in some unpredictable cases (if SD card is unmounted and 46 | * {@link Context#getCacheDir() Context.getCacheDir()} returns null). 47 | */ 48 | private static File getCacheDirectory(Context context, boolean preferExternal) { 49 | File appCacheDir = null; 50 | String externalStorageState; 51 | try { 52 | externalStorageState = Environment.getExternalStorageState(); 53 | } catch (NullPointerException e) { // (sh)it happens 54 | externalStorageState = ""; 55 | } 56 | if (preferExternal && MEDIA_MOUNTED.equals(externalStorageState)) { 57 | appCacheDir = getExternalCacheDir(context); 58 | } 59 | if (appCacheDir == null) { 60 | appCacheDir = context.getCacheDir(); 61 | } 62 | if (appCacheDir == null) { 63 | String cacheDirPath = "/data/data/" + context.getPackageName() + "/cache/"; 64 | LogU.d("Can't define system cache directory! '" + cacheDirPath + "%s' will be used."); 65 | appCacheDir = new File(cacheDirPath); 66 | } 67 | return appCacheDir; 68 | } 69 | 70 | private static File getExternalCacheDir(Context context) { 71 | File dataDir = new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data"); 72 | File appCacheDir = new File(new File(dataDir, context.getPackageName()), "cache"); 73 | if (!appCacheDir.exists()) { 74 | if (!appCacheDir.mkdirs()) { 75 | LogU.d("Unable to create external cache directory"); 76 | return null; 77 | } 78 | } 79 | return appCacheDir; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/file/DiskUsage.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.file; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | 6 | /** 7 | * Declares how {@link FileCache} will use disc space. 8 | * 9 | * @author Alexey Danilov (danikula@gmail.com). 10 | */ 11 | public interface DiskUsage { 12 | 13 | void touch(File file) throws IOException; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/file/FileCache.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.file; 2 | 3 | import com.danikula.videocache.Cache; 4 | import com.danikula.videocache.LogU; 5 | import com.danikula.videocache.ProxyCacheException; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.io.RandomAccessFile; 10 | 11 | /** 12 | * {@link Cache} that uses file for storing data. 13 | * 14 | * @author Alexey Danilov (danikula@gmail.com). 15 | */ 16 | public class FileCache implements Cache { 17 | 18 | private static final String TEMP_POSTFIX = ".download"; 19 | 20 | private final DiskUsage diskUsage; 21 | public File file; 22 | private RandomAccessFile dataFile; 23 | 24 | public FileCache(File file) throws ProxyCacheException { 25 | this(file, new UnlimitedDiskUsage()); 26 | } 27 | 28 | public FileCache(File file, DiskUsage diskUsage) throws ProxyCacheException { 29 | try { 30 | if (diskUsage == null) { 31 | throw new NullPointerException(); 32 | } 33 | this.diskUsage = diskUsage; 34 | File directory = file.getParentFile(); 35 | Files.makeDir(directory); 36 | boolean completed = file.exists(); 37 | this.file = completed ? file : new File(file.getParentFile(), file.getName() + TEMP_POSTFIX); 38 | this.dataFile = new RandomAccessFile(this.file, completed ? "r" : "rw"); 39 | long size = this.file.length(); 40 | LogU.d("临时文件大小"+size+" dataFile "+dataFile.length()+"file path"+file.getAbsolutePath()); 41 | } catch (IOException e) { 42 | throw new ProxyCacheException("Error using file " + file + " as disc cache", e); 43 | } 44 | } 45 | 46 | @Override 47 | public synchronized long available() throws ProxyCacheException { 48 | try { 49 | return (int) dataFile.length(); 50 | } catch (IOException e) { 51 | throw new ProxyCacheException("Error reading length of file " + file, e); 52 | } 53 | } 54 | 55 | @Override 56 | public synchronized int read(byte[] buffer, long offset, int length) throws ProxyCacheException { 57 | try { 58 | dataFile.seek(offset); 59 | LogU.d(":从文件缓存里读 "+offset+" "+length); 60 | return dataFile.read(buffer, 0, length); 61 | } catch (IOException e) { 62 | String format = "Error reading %d bytes with offset %d from file[%d bytes] to buffer[%d bytes]"; 63 | throw new ProxyCacheException(String.format(format, length, offset, available(), buffer.length), e); 64 | } 65 | } 66 | 67 | @Override 68 | public synchronized void append(byte[] data, int length) throws ProxyCacheException { 69 | try { 70 | if (isCompleted()) { 71 | throw new ProxyCacheException("Error append cache: cache file " + file + " is completed!"); 72 | } 73 | dataFile.seek(available()); 74 | dataFile.write(data, 0, length); 75 | } catch (IOException e) { 76 | String format = "Error writing %d bytes to %s from buffer with size %d"; 77 | throw new ProxyCacheException(String.format(format, length, dataFile, data.length), e); 78 | } 79 | } 80 | 81 | @Override 82 | public synchronized void close() throws ProxyCacheException { 83 | try { 84 | dataFile.close(); 85 | diskUsage.touch(file); 86 | } catch (IOException e) { 87 | throw new ProxyCacheException("Error closing file " + file, e); 88 | } 89 | } 90 | 91 | @Override 92 | public synchronized void complete() throws ProxyCacheException { 93 | if (isCompleted()) { 94 | return; 95 | } 96 | 97 | close(); 98 | String fileName = file.getName().substring(0, file.getName().length() - TEMP_POSTFIX.length()); 99 | File completedFile = new File(file.getParentFile(), fileName); 100 | boolean renamed = file.renameTo(completedFile); 101 | if (!renamed) { 102 | throw new ProxyCacheException("Error renaming file " + file + " to " + completedFile + " for completion!"); 103 | } 104 | file = completedFile; 105 | try { 106 | dataFile = new RandomAccessFile(file, "r"); 107 | diskUsage.touch(file); 108 | } catch (IOException e) { 109 | throw new ProxyCacheException("Error opening " + file + " as disc cache", e); 110 | } 111 | } 112 | 113 | @Override 114 | public synchronized boolean isCompleted() { 115 | return !isTempFile(file); 116 | } 117 | 118 | /** 119 | * Returns file to be used fo caching. It may as original file passed in constructor as some temp file for not completed cache. 120 | * 121 | * @return file for caching. 122 | */ 123 | public File getFile() { 124 | return file; 125 | } 126 | 127 | private boolean isTempFile(File file) { 128 | return file.getName().endsWith(TEMP_POSTFIX); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/file/FileNameGenerator.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.file; 2 | 3 | /** 4 | * Generator for files to be used for caching. 5 | * 6 | * @author Alexey Danilov (danikula@gmail.com). 7 | */ 8 | public interface FileNameGenerator { 9 | 10 | String generate(String url); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/file/Files.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.file; 2 | 3 | 4 | 5 | import com.danikula.videocache.LogU; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.io.RandomAccessFile; 10 | import java.util.Arrays; 11 | import java.util.Collections; 12 | import java.util.Comparator; 13 | import java.util.Date; 14 | import java.util.LinkedList; 15 | import java.util.List; 16 | 17 | /** 18 | * Utils for work with files. 19 | * 20 | * @author Alexey Danilov (danikula@gmail.com). 21 | */ 22 | class Files { 23 | 24 | 25 | 26 | static void makeDir(File directory) throws IOException { 27 | if (directory.exists()) { 28 | if (!directory.isDirectory()) { 29 | throw new IOException("File " + directory + " is not directory!"); 30 | } 31 | } else { 32 | boolean isCreated = directory.mkdirs(); 33 | if (!isCreated) { 34 | throw new IOException(String.format("Directory %s can't be created", directory.getAbsolutePath())); 35 | } 36 | } 37 | } 38 | 39 | static List getLruListFiles(File directory) { 40 | List result = new LinkedList<>(); 41 | File[] files = directory.listFiles(); 42 | if (files != null) { 43 | result = Arrays.asList(files); 44 | Collections.sort(result, new LastModifiedComparator()); 45 | } 46 | return result; 47 | } 48 | 49 | static void setLastModifiedNow(File file) throws IOException { 50 | if (file.exists()) { 51 | long now = System.currentTimeMillis(); 52 | boolean modified = file.setLastModified(now); // on some devices (e.g. Nexus 5) doesn't work 53 | if (!modified) { 54 | modify(file); 55 | if (file.lastModified() < now) { 56 | // NOTE: apparently this is a known issue (see: http://stackoverflow.com/questions/6633748/file-lastmodified-is-never-what-was-set-with-file-setlastmodified) 57 | LogU.d("Last modified date {} is not set for file {}"); 58 | } 59 | } 60 | } 61 | } 62 | 63 | static void modify(File file) throws IOException { 64 | long size = file.length(); 65 | if (size == 0) { 66 | recreateZeroSizeFile(file); 67 | return; 68 | } 69 | 70 | RandomAccessFile accessFile = new RandomAccessFile(file, "rwd"); 71 | accessFile.seek(size - 1); 72 | byte lastByte = accessFile.readByte(); 73 | accessFile.seek(size - 1); 74 | accessFile.write(lastByte); 75 | accessFile.close(); 76 | } 77 | 78 | private static void recreateZeroSizeFile(File file) throws IOException { 79 | if (!file.delete() || !file.createNewFile()) { 80 | throw new IOException("Error recreate zero-size file " + file); 81 | } 82 | } 83 | 84 | private static final class LastModifiedComparator implements Comparator { 85 | 86 | @Override 87 | public int compare(File lhs, File rhs) { 88 | return compareLong(lhs.lastModified(), rhs.lastModified()); 89 | } 90 | 91 | private int compareLong(long first, long second) { 92 | return (first < second) ? -1 : ((first == second) ? 0 : 1); 93 | } 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/file/LruDiskUsage.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.file; 2 | 3 | 4 | 5 | import com.danikula.videocache.LogU; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.util.List; 10 | import java.util.concurrent.Callable; 11 | import java.util.concurrent.ExecutorService; 12 | import java.util.concurrent.Executors; 13 | 14 | /** 15 | * {@link DiskUsage} that uses LRU (Least Recently Used) strategy to trim cache. 16 | * 17 | * @author Alexey Danilov (danikula@gmail.com). 18 | */ 19 | public abstract class LruDiskUsage implements DiskUsage { 20 | 21 | private final ExecutorService workerThread = Executors.newSingleThreadExecutor(); 22 | 23 | @Override 24 | public void touch(File file) throws IOException { 25 | workerThread.submit(new TouchCallable(file)); 26 | } 27 | 28 | private void touchInBackground(File file) throws IOException { 29 | Files.setLastModifiedNow(file); 30 | List files = Files.getLruListFiles(file.getParentFile()); 31 | trim(files); 32 | } 33 | 34 | protected abstract boolean accept(File file, long totalSize, int totalCount); 35 | 36 | private void trim(List files) { 37 | long totalSize = countTotalSize(files); 38 | int totalCount = files.size(); 39 | for (File file : files) { 40 | boolean accepted = accept(file, totalSize, totalCount); 41 | if (!accepted) { 42 | long fileSize = file.length(); 43 | boolean deleted = file.delete(); 44 | if (deleted) { 45 | totalCount--; 46 | totalSize -= fileSize; 47 | LogU.d("Cache file " + file + " is deleted because it exceeds cache limit"); 48 | } else { 49 | LogU.e("Error deleting file " + file + " for trimming cache"); 50 | } 51 | } 52 | } 53 | } 54 | 55 | private long countTotalSize(List files) { 56 | long totalSize = 0; 57 | for (File file : files) { 58 | totalSize += file.length(); 59 | } 60 | return totalSize; 61 | } 62 | 63 | private class TouchCallable implements Callable { 64 | 65 | private final File file; 66 | 67 | public TouchCallable(File file) { 68 | this.file = file; 69 | } 70 | 71 | @Override 72 | public Void call() throws Exception { 73 | touchInBackground(file); 74 | return null; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/file/Md5FileNameGenerator.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.file; 2 | 3 | import android.text.TextUtils; 4 | 5 | import com.danikula.videocache.ProxyCacheUtils; 6 | 7 | /** 8 | * Implementation of {@link FileNameGenerator} that uses MD5 of url as file name 9 | * 10 | * @author Alexey Danilov (danikula@gmail.com). 11 | */ 12 | public class Md5FileNameGenerator implements FileNameGenerator { 13 | 14 | private static final int MAX_EXTENSION_LENGTH = 4; 15 | 16 | @Override 17 | public String generate(String url) { 18 | String extension = getExtension(url); 19 | String name = ProxyCacheUtils.computeMD5(url); 20 | return TextUtils.isEmpty(extension) ? name : name + "." + extension; 21 | } 22 | 23 | private String getExtension(String url) { 24 | int dotIndex = url.lastIndexOf('.'); 25 | int slashIndex = url.lastIndexOf('/'); 26 | return dotIndex != -1 && dotIndex > slashIndex && dotIndex + 2 + MAX_EXTENSION_LENGTH > url.length() ? 27 | url.substring(dotIndex + 1, url.length()) : ""; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/file/TotalCountLruDiskUsage.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.file; 2 | 3 | import java.io.File; 4 | 5 | /** 6 | * {@link DiskUsage} that uses LRU (Least Recently Used) strategy and trims cache size to max files count if needed. 7 | * 8 | * @author Alexey Danilov (danikula@gmail.com). 9 | */ 10 | public class TotalCountLruDiskUsage extends LruDiskUsage { 11 | 12 | private final int maxCount; 13 | 14 | public TotalCountLruDiskUsage(int maxCount) { 15 | if (maxCount <= 0) { 16 | throw new IllegalArgumentException("Max count must be positive number!"); 17 | } 18 | this.maxCount = maxCount; 19 | } 20 | 21 | @Override 22 | protected boolean accept(File file, long totalSize, int totalCount) { 23 | return totalCount <= maxCount; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/file/TotalSizeLruDiskUsage.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.file; 2 | 3 | import java.io.File; 4 | 5 | /** 6 | * {@link DiskUsage} that uses LRU (Least Recently Used) strategy and trims cache size to max size if needed. 7 | * 8 | * @author Alexey Danilov (danikula@gmail.com). 9 | */ 10 | public class TotalSizeLruDiskUsage extends LruDiskUsage { 11 | 12 | private final long maxSize; 13 | 14 | public TotalSizeLruDiskUsage(long maxSize) { 15 | if (maxSize <= 0) { 16 | throw new IllegalArgumentException("Max size must be positive number!"); 17 | } 18 | this.maxSize = maxSize; 19 | } 20 | 21 | @Override 22 | protected boolean accept(File file, long totalSize, int totalCount) { 23 | return totalSize <= maxSize; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/file/UnlimitedDiskUsage.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.file; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | 6 | /** 7 | * Unlimited version of {@link DiskUsage}. 8 | * 9 | * @author Alexey Danilov (danikula@gmail.com). 10 | */ 11 | public class UnlimitedDiskUsage implements DiskUsage { 12 | 13 | @Override 14 | public void touch(File file) throws IOException { 15 | // do nothing 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/headers/EmptyHeadersInjector.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.headers; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * Empty {@link HeaderInjector} implementation. 8 | * 9 | * @author Lucas Nelaupe (https://github.com/lucas34). 10 | */ 11 | public class EmptyHeadersInjector implements HeaderInjector { 12 | 13 | @Override 14 | public Map addHeaders(String url) { 15 | return new HashMap<>(); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/headers/HeaderInjector.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.headers; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * Allows to add custom headers to server's requests. 7 | * 8 | * @author Lucas Nelaupe (https://github.com/lucas34). 9 | */ 10 | public interface HeaderInjector { 11 | 12 | /** 13 | * Adds headers to server's requests for corresponding url. 14 | * 15 | * @param url an url headers will be added for 16 | * @return a map with headers, where keys are header's names, and values are header's values. {@code null} is not acceptable! 17 | */ 18 | Map addHeaders(String url); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/sourcestorage/DatabaseSourceInfoStorage.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.sourcestorage; 2 | 3 | import android.content.ContentValues; 4 | import android.content.Context; 5 | import android.database.Cursor; 6 | import android.database.sqlite.SQLiteDatabase; 7 | import android.database.sqlite.SQLiteOpenHelper; 8 | 9 | import com.danikula.videocache.SourceInfo; 10 | 11 | import static com.danikula.videocache.Preconditions.checkAllNotNull; 12 | import static com.danikula.videocache.Preconditions.checkNotNull; 13 | 14 | /** 15 | * Database based {@link SourceInfoStorage}. 16 | * 17 | * @author Alexey Danilov (danikula@gmail.com). 18 | */ 19 | class DatabaseSourceInfoStorage extends SQLiteOpenHelper implements SourceInfoStorage { 20 | 21 | private static final String TABLE = "SourceInfo"; 22 | private static final String COLUMN_ID = "_id"; 23 | private static final String COLUMN_URL = "url"; 24 | private static final String COLUMN_LENGTH = "length"; 25 | private static final String COLUMN_MIME = "mime"; 26 | private static final String[] ALL_COLUMNS = new String[]{COLUMN_ID, COLUMN_URL, COLUMN_LENGTH, COLUMN_MIME}; 27 | private static final String CREATE_SQL = 28 | "CREATE TABLE " + TABLE + " (" + 29 | COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," + 30 | COLUMN_URL + " TEXT NOT NULL," + 31 | COLUMN_MIME + " TEXT," + 32 | COLUMN_LENGTH + " INTEGER" + 33 | ");"; 34 | 35 | DatabaseSourceInfoStorage(Context context) { 36 | super(context, "AndroidVideoCache.db", null, 1); 37 | checkNotNull(context); 38 | } 39 | 40 | @Override 41 | public void onCreate(SQLiteDatabase db) { 42 | checkNotNull(db); 43 | db.execSQL(CREATE_SQL); 44 | } 45 | 46 | @Override 47 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 48 | throw new IllegalStateException("Should not be called. There is no any migration"); 49 | } 50 | 51 | @Override 52 | public SourceInfo get(String url) { 53 | checkNotNull(url); 54 | Cursor cursor = null; 55 | try { 56 | cursor = getReadableDatabase().query(TABLE, ALL_COLUMNS, COLUMN_URL + "=?", new String[]{url}, null, null, null); 57 | return cursor == null || !cursor.moveToFirst() ? null : convert(cursor); 58 | } finally { 59 | if (cursor != null) { 60 | cursor.close(); 61 | } 62 | } 63 | } 64 | 65 | @Override 66 | public void put(String url, SourceInfo sourceInfo) { 67 | checkAllNotNull(url, sourceInfo); 68 | SourceInfo sourceInfoFromDb = get(url); 69 | boolean exist = sourceInfoFromDb != null; 70 | ContentValues contentValues = convert(sourceInfo); 71 | if (exist) { 72 | getWritableDatabase().update(TABLE, contentValues, COLUMN_URL + "=?", new String[]{url}); 73 | } else { 74 | getWritableDatabase().insert(TABLE, null, contentValues); 75 | } 76 | } 77 | 78 | @Override 79 | public void release() { 80 | close(); 81 | } 82 | 83 | private SourceInfo convert(Cursor cursor) { 84 | return new SourceInfo( 85 | cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_URL)), 86 | cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_LENGTH)), 87 | cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MIME)) 88 | ); 89 | } 90 | 91 | private ContentValues convert(SourceInfo sourceInfo) { 92 | ContentValues values = new ContentValues(); 93 | values.put(COLUMN_URL, sourceInfo.url); 94 | values.put(COLUMN_LENGTH, sourceInfo.length); 95 | values.put(COLUMN_MIME, sourceInfo.mime); 96 | return values; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/sourcestorage/NoSourceInfoStorage.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.sourcestorage; 2 | 3 | import com.danikula.videocache.SourceInfo; 4 | 5 | /** 6 | * {@link SourceInfoStorage} that does nothing. 7 | * 8 | * @author Alexey Danilov (danikula@gmail.com). 9 | */ 10 | public class NoSourceInfoStorage implements SourceInfoStorage { 11 | 12 | @Override 13 | public SourceInfo get(String url) { 14 | return null; 15 | } 16 | 17 | @Override 18 | public void put(String url, SourceInfo sourceInfo) { 19 | } 20 | 21 | @Override 22 | public void release() { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/sourcestorage/SourceInfoStorage.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.sourcestorage; 2 | 3 | import com.danikula.videocache.SourceInfo; 4 | 5 | /** 6 | * Storage for {@link SourceInfo}. 7 | * 8 | * @author Alexey Danilov (danikula@gmail.com). 9 | */ 10 | public interface SourceInfoStorage { 11 | 12 | SourceInfo get(String url); 13 | 14 | void put(String url, SourceInfo sourceInfo); 15 | 16 | void release(); 17 | } 18 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/sourcestorage/SourceInfoStorageFactory.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.sourcestorage; 2 | 3 | import android.content.Context; 4 | 5 | /** 6 | * Simple factory for {@link SourceInfoStorage}. 7 | * 8 | * @author Alexey Danilov (danikula@gmail.com). 9 | */ 10 | public class SourceInfoStorageFactory { 11 | 12 | public static SourceInfoStorage newSourceInfoStorage(Context context) { 13 | return new DatabaseSourceInfoStorage(context); 14 | } 15 | 16 | public static SourceInfoStorage newEmptySourceInfoStorage() { 17 | return new NoSourceInfoStorage(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /videocache/src/test/java/com/danikula/videocache/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | --------------------------------------------------------------------------------