Commit 809a56d2 authored by jyx's avatar jyx

删除饺子播放器,优化样式

parent 49914de3
......@@ -212,7 +212,6 @@ dependencies {
// 悬浮窗
implementation 'com.github.princekin-f:EasyFloat:1.3.4'
implementation 'com.airbnb.android:lottie:3.4.0'
implementation 'cn.jzvd:jiaozivideoplayer:7.7.0'
// 工具类
// BASE64Decoder接入
// 兼容奔溃问题-Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent.
......@@ -223,8 +222,6 @@ dependencies {
api project(':wxpay')
api project(':alipay')
api project(':oaid')
//缓存
api project(":videocache")
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4'
// bugly
implementation 'com.tencent.bugly:crashreport:3.3.92'
......
......@@ -7,7 +7,6 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.multidex.MultiDex;
import androidx.multidex.MultiDexApplication;
import com.danikula.videocache.HttpProxyCacheServer;
import com.downloader.PRDownloader;
import com.downloader.PRDownloaderConfig;
import com.xinfu.helivideo.ad.TTGroMoreAdManagerHolder;
......@@ -41,21 +40,22 @@ public class MintsApplication extends MultiDexApplication {
private Scheduler defaultSubscribeScheduler;
private LoanService loanService;
private V6Service v6Service;
private HttpProxyCacheServer proxy = null;
public static class StaticParams {
public static HttpProxyCacheServer getProxy() {
MintsApplication app = (MintsApplication) MintsApplication.getContext();
return app.proxy == null ? app.newProxy() : app.proxy;
}
}
private HttpProxyCacheServer newProxy() {
return new HttpProxyCacheServer.Builder(this)
.maxCacheSize(1024 * 1024 * 1024)
.maxCacheFilesCount(30)
.build();
}
// private HttpProxyCacheServer proxy = null;
//
// public static class StaticParams {
// public static HttpProxyCacheServer getProxy() {
// MintsApplication app = (MintsApplication) MintsApplication.getContext();
// return app.proxy == null ? app.newProxy() : app.proxy;
// }
// }
//
// private HttpProxyCacheServer newProxy() {
// return new HttpProxyCacheServer.Builder(this)
// .maxCacheSize(1024 * 1024 * 1024)
// .maxCacheFilesCount(30)
// .build();
// }
/**
......
......@@ -14,7 +14,6 @@ import com.xinfu.helivideo.utils.AppPreferencesManager
import com.xinfu.helivideo.utils.ToastUtil
import com.xinfu.helivideo.video.DramaApiDetailActivity
import com.xinfu.helivideo.video.TxVideoActivity
import com.xinfu.helivideo.video.VideoActivity
/**
* 本地视频缓存管理
......
package com.xinfu.helivideo.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.OrientationHelper
import androidx.recyclerview.widget.RecyclerView
import cn.jzvd.Jzvd
import com.xinfu.helivideo.R
import com.xinfu.helivideo.manager.LocalVedioManager
import com.xinfu.helivideo.mvp.model.BannerList
import com.xinfu.helivideo.mvp.model.VedioBean
import com.xinfu.helivideo.mvp.presenters.RecommendPresenter
import com.xinfu.helivideo.mvp.views.RecommendView
import com.xinfu.helivideo.ui.fragment.base.BaseFragment
import com.xinfu.helivideo.video.*
import kotlinx.android.synthetic.main.fragment_watch_video.recy
/**
* @author Assen
* @date 2023/7/5
* @desc 主页 -> 推荐 —> 推荐
*/
class WatchVideoFragment : BaseFragment(), RecommendView {
private val recommendPresenter by lazy { RecommendPresenter() }
companion object {
fun newInstance(): Fragment {
val args = Bundle()
val fragment = WatchVideoFragment()
fragment.arguments = args
return fragment
}
}
private var mCurrentPosition = 0
lateinit var adapter: RecommendVideoAdapter
var videos = arrayListOf<VedioBean>()
override fun initViewsAndEvents() {
recommendPresenter.attachView(this)
val layoutManager = RecyViewLayoutManager(requireContext(), OrientationHelper.VERTICAL)
recy.layoutManager = layoutManager
adapter = RecommendVideoAdapter(requireActivity())
recy.adapter = adapter
//预加载下一个
adapter.setNewInstance(videos)
val emptyView =
LayoutInflater.from(requireContext()).inflate(R.layout.item_empty_video, null)
adapter.setEmptyView(emptyView)
adapter.addChildClickViewIds(R.id.ll_bottom, R.id.ll_collect)
adapter.setOnItemChildClickListener { adapter, view, position ->
when (view.id) {
R.id.ll_bottom -> {
Jzvd.goOnPlayOnPause()
videos[position].recommendIndex++
LocalVedioManager.startVedioDetailActivityForType(
requireActivity(),
videos[position],
true
)
}
R.id.ll_collect -> {
if (videos[position].collect == 0) {
videos[position].collect = 1
recommendPresenter.collect("" + videos[position].vedioId)
} else {
recommendPresenter.cancelCollect("" + videos[position].vedioId)
videos[position].collect = 0
}
}
else -> {}
}
}
adapter.setOnVideoCompletion(object : JzvdStdTikTok.OnVideoCompletion {
override fun onVideoCompletion() {
// showToast("即将为您播放下一集")
Jzvd.goOnPlayOnPause()
videos[mCurrentPosition].recommendIndex++
LocalVedioManager.startVedioDetailActivityForType(
requireActivity(),
videos[mCurrentPosition],
true
)
}
})
layoutManager.setOnViewPagerListener(object : OnRecyViewListener {
override fun onInitComplete() {
//初始化 自动播放
autoPlayVideo()
}
override fun onPageRelease(isNext: Boolean, position: Int) {
//滑动时,释放上一个
if (mCurrentPosition == position) {
Jzvd.releaseAllVideos()
}
}
override fun onPageSelected(position: Int, isBottom: Boolean) {
//滑动后的当前Item ,具体自行打印
if (mCurrentPosition == position) {
return
}
if (isBottom) {
//是最底部,执行加载更多数据
}
autoPlayVideo()
mCurrentPosition = position
}
})
///监听item离开了屏幕
recy.addOnChildAttachStateChangeListener(object :
RecyclerView.OnChildAttachStateChangeListener {
override fun onChildViewDetachedFromWindow(view: View) {
if (view !is ConstraintLayout) return
val jzvd: Jzvd = view.findViewById(R.id.jz_video)
if (jzvd != null && Jzvd.CURRENT_JZVD != null &&
jzvd.jzDataSource.containsTheUrl(Jzvd.CURRENT_JZVD.jzDataSource.currentUrl)
) {
if (Jzvd.CURRENT_JZVD != null && Jzvd.CURRENT_JZVD.screen != Jzvd.SCREEN_FULLSCREEN) {
Jzvd.releaseAllVideos()
}
}
}
override fun onChildViewAttachedToWindow(view: View) {
}
})
}
override fun getContentViewLayoutID() = R.layout.fragment_watch_video
override fun onResume() {
super.onResume()
Jzvd.goOnPlayOnResume()
recommendPresenter.autoList()
}
override fun onPause() {
super.onPause()
Jzvd.goOnPlayOnPause()
}
override fun onDestroy() {
super.onDestroy()
Jzvd.releaseAllVideos()
}
/**
* 滑动后自动播放。
*/
private fun autoPlayVideo() {
if (recy == null || recy.getChildAt(0) == null) {
return
}
if (recy.getChildAt(0) !is ConstraintLayout) return
val player: JzvdStdTikTok = recy.getChildAt(0).findViewById(R.id.jz_video)
if (player != null) {
player.startVideoAfterPreloading()
}
}
override fun onDetach() {
super.onDetach()
recommendPresenter.detachView()
}
override fun collectSuc() {
}
override fun collectFail() {
}
override fun cancelCollectSuc() {
}
override fun cancelCollectFail() {
}
override fun autoListSuc(list: BannerList) {
videos.clear()
videos.addAll(list.list)
adapter?.notifyDataSetChanged()
}
override fun autoListFail() {
}
}
\ No newline at end of file
......@@ -6,9 +6,7 @@ import com.bytedance.sdk.dp.DPSdk
import com.bytedance.sdk.dp.DPSdkConfig
/**
* @author Assen
* @date 2023/6/21
* @desc
* 穿山甲短剧SDK
*/
object DPHolderManager {
......
package com.xinfu.helivideo.video
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.Gravity
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.Toast
import cn.jzvd.Jzvd
import cn.jzvd.JzvdStd
import com.xinfu.helivideo.R
import com.xinfu.helivideo.utils.UIUtils
/**
* author : ChenWenJie
* email : 1181620038@qq.com
* date : 2020/9/22
* desc : 重写播放器,方便控制。监听播放器状态。
*/
class JzvdStdTikTok : JzvdStd {
constructor(context: Context?) : super(context) {}
constructor(context: Context?, attrs: AttributeSet?) : super(
context,
attrs
) {
}
override fun init(context: Context?) {
super.init(context)
bottomContainer.visibility = View.VISIBLE
currentTimeTextView.visibility = View.GONE
totalTimeTextView.visibility = View.GONE //当前时间
fullscreenButton.visibility = View.GONE //放大按钮
topContainer.visibility = View.GONE
loadingProgressBar.visibility = View.GONE //加载loaing
progressBar.visibility = View.VISIBLE //控制的
posterImageView.scaleType = ImageView.ScaleType.FIT_CENTER
bottomProgressBar.visibility = View.GONE //最底部的进度
val layoutParams = progressBar.layoutParams as LinearLayout.LayoutParams
layoutParams.gravity = Gravity.BOTTOM
layoutParams.height = UIUtils.dip2px(context, 6f)
progressBar.thumb = context?.getDrawable(R.drawable.jz_bottom_seek_poster)
progressBar.progressDrawable = context?.getDrawable(R.drawable.jz_bottom_seek_progress)
progressBar.layoutParams = layoutParams
progressBar.setPadding(0, 0, 0, 0)
val startLayout = findViewById<LinearLayout>(cn.jzvd.R.id.start_layout)
val layoutParams2 = startLayout.layoutParams as RelativeLayout.LayoutParams
layoutParams2.width = UIUtils.dip2px(context, 100f)
layoutParams2.height = UIUtils.dip2px(context, 100f)
startLayout.layoutParams = layoutParams2
startLayout.gravity = Gravity.CENTER
val myStart = findViewById<ImageView>(cn.jzvd.R.id.start)
myStart.scaleX = 1.5f
myStart.scaleY = 1.5f
}
override fun setUp(
url: String?,
title: String?,
screen: Int,
mediaInterfaceClass: Class<*>?
) {
super.setUp(url, title, screen)
}
//changeUiTo 真能能修改ui的方法
override fun changeUiToNormal() {
super.changeUiToNormal()
bottomContainer.visibility = View.VISIBLE
topContainer.visibility = View.GONE
// mDialogProgressBar.setVisibility(GONE);
}
override fun setAllControlsVisiblity(
topCon: Int, bottomCon: Int, startBtn: Int, loadingPro: Int,
posterImg: Int, bottomPro: Int, retryLayout: Int
) {
topContainer.visibility = topCon
// bottomContainer.visibility = bottomCon
startButton.visibility = startBtn
loadingProgressBar.visibility = View.GONE
posterImageView.visibility = posterImg
bottomProgressBar.visibility = View.GONE
mRetryLayout.visibility = retryLayout
bottomContainer.visibility = View.VISIBLE
}
override fun dissmissControlView() {
if (state != Jzvd.STATE_NORMAL && state != Jzvd.STATE_ERROR && state != Jzvd.STATE_AUTO_COMPLETE
) {
post {
// bottomContainer.visibility = View.INVISIBLE
topContainer.visibility = View.INVISIBLE
startButton.visibility = View.INVISIBLE
if (clarityPopWindow != null) {
clarityPopWindow.dismiss()
}
if (screen != Jzvd.SCREEN_TINY) {
bottomProgressBar.visibility = View.GONE
}
}
}
}
override fun onClickUiToggle() {
super.onClickUiToggle()
Log.i(Jzvd.TAG, "click blank")
startButton.performClick()
bottomContainer.visibility = View.VISIBLE
topContainer.visibility = View.GONE
}
override fun onStateNormal() {
super.onStateNormal()
}
override fun onStatePreparing() {
super.onStatePreparing()
Log.e("onStatePreparing", " 准备")
}
override fun onStatePlaying() {
super.onStatePlaying()
val times = duration
Log.e("onStatePlaying", " $times")
// 处理切换页面进度不展示问题
mChangePosition = true
touchActionUp()
}
override fun onStatePause() {
super.onStatePause()
Log.e("onStatePause:", "暂停")
}
override fun onStateError() {
super.onStateError()
Log.e("onStateError:", "错误")
}
//播放完成自动播放
override fun onStateAutoComplete() {
Toast.makeText(applicationContext, "onStateAutoComplete", Toast.LENGTH_SHORT).show()
}
override fun onCompletion() {
// RxBus.INSTANCE.post(RXCmCWJ(121))
onVideoCompletion?.onVideoCompletion()
}
override fun updateStartImage() {
if (state == Jzvd.STATE_PLAYING) {
startButton.visibility = View.VISIBLE
startButton.setImageResource(R.drawable.tiktok_play_tiktok)
replayTextView.visibility = View.GONE
} else if (state == Jzvd.STATE_ERROR) {
startButton.visibility = View.INVISIBLE
replayTextView.visibility = View.GONE
} else if (state == Jzvd.STATE_AUTO_COMPLETE) {
startButton.visibility = View.VISIBLE
startButton.setImageResource(R.drawable.tiktok_play_tiktok)
replayTextView.visibility = View.GONE
} else {
startButton.setImageResource(R.drawable.tiktok_play_tiktok)
replayTextView.visibility = View.GONE
}
}
fun setOnVideoCompletion(onVideoCompletion: OnVideoCompletion?) {
this.onVideoCompletion = onVideoCompletion
}
private var onVideoCompletion: OnVideoCompletion? = null
interface OnVideoCompletion {
fun onVideoCompletion()
}
}
package com.xinfu.helivideo.video
import android.app.Activity
import android.util.Log
import android.view.View
import android.widget.ImageView
import cn.jzvd.Jzvd
import cn.jzvd.JzvdStd
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieDrawable
import com.bumptech.glide.Glide
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import com.xinfu.helivideo.MintsApplication
import com.xinfu.helivideo.R
import com.xinfu.helivideo.mvp.model.VedioBean
/**
* author : ChenWenJie
* email : 1181620038@qq.com
* date : 2020/9/22
* desc : 适配器
*/
class RecommendVideoAdapter(var activity: Activity) :
BaseQuickAdapter<VedioBean, BaseViewHolder>(R.layout.item_video_recommend) {
override fun convert(holder: BaseViewHolder, item: VedioBean) {
if (item.completeStatus == 0) {
holder.setText(R.id.episode_tv, "共" + item.vedioTotal + "集 已完结")
} else {
holder.setText(R.id.episode_tv, "共" + item.vedioTotal + "集 更新中")
}
//用户名
holder.setText(R.id.username_tv, item.title)
//标题
holder.setText(R.id.usertitle_tv, "第" + item.recommendIndex + "集")
//缩略图
val posterImageView = holder.getView<JzvdStdTikTok>(R.id.jz_video).posterImageView
posterImageView.scaleType = ImageView.ScaleType.FIT_XY
Glide.with(context).load(item.coverImage)
.into(posterImageView)
if (item.collect == 0) {
// 未收藏
holder.getView<LottieAnimationView>(R.id.iv_collect)
.setImageResource(R.mipmap.home_collect_img_0)
} else {
// 已收藏
holder.getView<LottieAnimationView>(R.id.iv_collect)
.setImageResource(R.mipmap.home_collect_img_1)
}
// 热度
holder.setText(R.id.tv_collect_num, item.hot)
var isPlay = false
holder.getView<View>(R.id.ll_collect).setOnClickListener {
isPlay = !isPlay
if (isPlay) {
playCollectAnim(holder.getView(R.id.iv_collect))
} else {
playCancelCollectAnim(holder.getView(R.id.iv_collect))
}
}
//声明 代理服务缓存
val proxy = MintsApplication.StaticParams.getProxy()
//这个缓存下一个
if (holder.layoutPosition + 1 < itemCount) {
val item1 = getItem(holder.layoutPosition + 1)
//缓存下一个 10秒
proxy!!.preLoad(item1.recommendUrl, 10)
}
//缓存当前,播放当前
val proxyUrl = proxy?.getProxyUrl(item.recommendUrl).toString() //设置视
// setPlay(holder.getView(R.id.jz_video), proxyUrl)
setPlay(holder.getView(R.id.jz_video), item.recommendUrl)
}
fun setPlay(jzvdStdTikTok: JzvdStdTikTok, path: String) {
Log.e("RecommendVideoAdapter", "$path")
//不保存播放进度
Jzvd.SAVE_PROGRESS = false
//取消播放时在非WIFIDialog提示
Jzvd.WIFI_TIP_DIALOG_SHOWED = true
// 清除某个URL进度
//JZUtils.clearSavedProgress(activity, path)
jzvdStdTikTok.setUp(path, "", JzvdStd.SCREEN_NORMAL)
// 设置全屏拉伸
Jzvd.setVideoImageDisplayType(Jzvd.VIDEO_IMAGE_DISPLAY_TYPE_FILL_PARENT)
jzvdStdTikTok.setOnVideoCompletion(onVideoCompletion)
}
private var onVideoCompletion: JzvdStdTikTok.OnVideoCompletion? = null
fun setOnVideoCompletion(onVideoCompletion: JzvdStdTikTok.OnVideoCompletion?) {
this.onVideoCompletion = onVideoCompletion
}
private fun playCollectAnim(view: LottieAnimationView) {
val lottieDrawable = LottieDrawable()
LottieCompositionFactory.fromAsset(context, "home_collect.json")
.addListener { result: LottieComposition? ->
lottieDrawable.setImagesAssetsFolder("images/")
lottieDrawable.composition = result
lottieDrawable.loop(false)
lottieDrawable.playAnimation()
}
view.setImageDrawable(lottieDrawable)
}
private fun playCancelCollectAnim(view: LottieAnimationView) {
val lottieDrawable = LottieDrawable()
LottieCompositionFactory.fromAsset(context, "home_cancel_collect.json")
.addListener { result: LottieComposition? ->
lottieDrawable.setImagesAssetsFolder("images/")
lottieDrawable.composition = result
lottieDrawable.loop(false)
lottieDrawable.playAnimation()
}
view.setImageDrawable(lottieDrawable)
}
}
\ No newline at end of file
package com.xinfu.helivideo.video
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import cn.jzvd.Jzvd
import cn.jzvd.JzvdStd
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieDrawable
import com.bumptech.glide.Glide
import com.chad.library.adapter.base.BaseMultiItemQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import com.xinfu.helivideo.MintsApplication
import com.xinfu.helivideo.R
import com.xinfu.helivideo.mvp.model.VedioBean
import com.xinfu.helivideo.mvp.model.VideoMultiItemEntity
/**
* author : ChenWenJie
* email : 1181620038@qq.com
* date : 2020/9/22
* desc : 适配器
*/
class VideoAdapter(private var vedioBean: VedioBean) :
BaseMultiItemQuickAdapter<VideoMultiItemEntity, BaseViewHolder>() {
init {
addItemType(VideoMultiItemEntity.MULTI_ITEM_1, R.layout.item_video)
addItemType(VideoMultiItemEntity.MULTI_ITEM_2, R.layout.item_block_view)
}
override fun convert(holder: BaseViewHolder, item: VideoMultiItemEntity) {
if (holder.itemViewType == VideoMultiItemEntity.MULTI_ITEM_1) {
initVideoHolder(holder, item)
} else {
initLockHolder(holder, item)
}
}
private fun initLockHolder(holder: BaseViewHolder, item: VideoMultiItemEntity) {
Glide.with(context).load(vedioBean.coverImage)
.into(holder.getView(R.id.iv_bg))
holder.getView<View>(R.id.vip).setOnClickListener {
mOnCustomChildClickListener?.onCustomChildClick(it, holder.adapterPosition)
}
holder.getView<TextView>(R.id.unlock).text = "看广告解锁" + vedioBean!!.adGiveVedioNum + "集"
holder.getView<View>(R.id.leave).visibility = View.GONE
holder.getView<View>(R.id.leave).setOnClickListener {
mOnCustomChildClickListener?.onCustomChildClick(it, holder.adapterPosition)
}
holder.getView<View>(R.id.unlock).setOnClickListener {
mOnCustomChildClickListener?.onCustomChildClick(it, holder.adapterPosition)
}
}
private fun initVideoHolder(holder: BaseViewHolder, item: VideoMultiItemEntity) {
//标题
holder.setText(R.id.title_tv, vedioBean.title)
//介绍
holder.setText(R.id.info_tv, "第" + item.video.vedioIndex + "集")
//收藏数量
holder.setText(R.id.zan_num_tv, "" + vedioBean.hot)
//缩略图
val posterImageView = holder.getView<JzvdStdTikTok>(R.id.jz_video).posterImageView
posterImageView.scaleType=ImageView.ScaleType.FIT_XY
Glide.with(context).load(vedioBean.coverImage)
.into(posterImageView)
holder.getView<LinearLayout>(R.id.ll_collect).setOnClickListener {
mOnCustomChildClickListener?.onCustomChildClick(it, holder.adapterPosition)
if (vedioBean.collect == 0) {
vedioBean.collect = 1
playCollectAnim(holder.getView(R.id.zan_iv))
} else {
vedioBean.collect = 0
playCancelCollectAnim(holder.getView(R.id.zan_iv))
}
}
if (vedioBean.collect == 0) {
// 未收藏
holder.getView<LottieAnimationView>(R.id.zan_iv)
.setImageResource(R.mipmap.home_collect_img_0)
} else {
// 已收藏
holder.getView<LottieAnimationView>(R.id.zan_iv)
.setImageResource(R.mipmap.home_collect_img_1)
}
//声明 代理服务缓存
val proxy = MintsApplication.StaticParams.getProxy()
//这个缓存下一个
if (holder.layoutPosition + 1 < itemCount) {
val item1 = getItem(holder.layoutPosition + 1)
//缓存下一个 10秒
proxy!!.preLoad(item1.video.vedioUrl, 10)
}
//缓存当前,播放当前
val proxyUrl = proxy?.getProxyUrl(item.video.vedioUrl).toString() //设置视
// setPlay(holder.getView(R.id.jz_video), proxyUrl)
setPlay(holder.getView(R.id.jz_video), item.video.vedioUrl)
}
fun setPlay(jzvdStdTikTok: JzvdStdTikTok, path: String) {
Log.e("VideoAdapter", "$path")
//不保存播放进度
Jzvd.SAVE_PROGRESS = false
//取消播放时在非WIFIDialog提示
Jzvd.WIFI_TIP_DIALOG_SHOWED = true
// 清除某个URL进度
//JZUtils.clearSavedProgress(activity, path)
jzvdStdTikTok.setUp(path, "", JzvdStd.SCREEN_NORMAL)
// 设置全屏拉伸
Jzvd.setVideoImageDisplayType(Jzvd.VIDEO_IMAGE_DISPLAY_TYPE_FILL_PARENT)
jzvdStdTikTok.setOnVideoCompletion(onVideoCompletion)
}
private var onVideoCompletion: JzvdStdTikTok.OnVideoCompletion? = null
fun setOnVideoCompletion(onVideoCompletion: JzvdStdTikTok.OnVideoCompletion?) {
this.onVideoCompletion = onVideoCompletion
}
private fun playCollectAnim(view: LottieAnimationView) {
val lottieDrawable = LottieDrawable()
LottieCompositionFactory.fromAsset(context, "home_collect.json")
.addListener { result: LottieComposition? ->
lottieDrawable.setImagesAssetsFolder("images/")
lottieDrawable.composition = result
lottieDrawable.loop(false)
lottieDrawable.playAnimation()
}
view.setImageDrawable(lottieDrawable)
}
private fun playCancelCollectAnim(view: LottieAnimationView) {
val lottieDrawable = LottieDrawable()
LottieCompositionFactory.fromAsset(context, "home_cancel_collect.json")
.addListener { result: LottieComposition? ->
lottieDrawable.setImagesAssetsFolder("images/")
lottieDrawable.composition = result
lottieDrawable.loop(false)
lottieDrawable.playAnimation()
}
view.setImageDrawable(lottieDrawable)
}
private var mOnCustomChildClickListener: OnCustomChildClickListener? = null
fun setOnCustomChildClickListener(onCustomChildClickListener: OnCustomChildClickListener) {
this.mOnCustomChildClickListener = onCustomChildClickListener
}
interface OnCustomChildClickListener {
fun onCustomChildClick(view: View, position: Int)
}
}
\ No newline at end of file
......@@ -9,7 +9,7 @@ import java.util.List;
import java.util.Map;
public class PlayerManager {
private static final String TAG = "ShortVideoDemo:PlayerManager";
private static final String TAG = "PlayerManager";
private final static int sMaxPlayerSize = 10;
private Map<VideoModel, TXVodPlayerWrapper> mUrlPlayerMap;
......
......@@ -53,11 +53,9 @@ class TxVideoFragment : BaseFragment(), RecommendView {
}
R.id.ll_collect -> {
if (videos[position].collect == 0) {
videos[position].collect = 1
recommendPresenter.collect("" + videos[position].vedioId)
} else {
recommendPresenter.cancelCollect("" + videos[position].vedioId)
videos[position].collect = 0
}
}
else -> {}
......
......@@ -42,11 +42,9 @@ class TxRecommendVideoAdapter :
// 热度
holder.setText(R.id.tv_collect_num, item.hot)
var isPlay = false
holder.getView<View>(R.id.ll_collect).setOnClickListener {
mOnCustomChildClickListener?.onCustomChildClick(it, holder.adapterPosition)
isPlay = !isPlay
if (isPlay) {
if (item.collect == 0) {
playCollectAnim(holder.getView(R.id.iv_collect))
} else {
playCancelCollectAnim(holder.getView(R.id.iv_collect))
......
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical"
tools:context=".video.VideoActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recy"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="50dp" />
<ImageView
android:id="@+id/close_iv"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="20dp"
android:padding="10dp"
android:src="@mipmap/ic_arrow_white" />
<FrameLayout
android:id="@+id/fm_bottom"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_gravity="bottom">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_40"
android:layout_gravity="center"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:background="@drawable/shape_half_trans"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/dp_10"
android:paddingEnd="@dimen/dp_10">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@mipmap/ic_video_epsiode" />
<TextView
android:id="@+id/episode_tv"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:text="共100集 已完结"
android:textColor="@color/white" />
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@mipmap/ic_arrow_top" />
</LinearLayout>
</FrameLayout>
</FrameLayout>
\ No newline at end of file
......@@ -126,14 +126,14 @@
android:layout_height="48dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
app:tabBackground="@color/full_transparent"
app:tabBackground="@null"
app:tabIndicatorHeight="0dp"
app:tabMaxWidth="200dp"
app:tabMinWidth="20dp"
app:tabMode="scrollable"
app:tabPaddingEnd="6dp"
app:tabPaddingStart="6dp"
app:tabRippleColor="@color/full_transparent" />
app:tabRippleColor="@null" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/vp2"
......@@ -161,15 +161,15 @@
<ImageView
android:id="@+id/iv_main_watching_pic"
android:layout_width="60dp"
android:scaleType="fitXY"
android:layout_height="70dp"
android:layout_margin="5dp" />
android:layout_margin="5dp"
android:scaleType="fitXY" />
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
......@@ -177,8 +177,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="我去平行时空我去平行时空"
android:singleLine="true"
android:text="我去平行时空我去平行时空"
android:textColor="@color/white"
android:textSize="14sp" />
......@@ -187,8 +187,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:singleLine="true"
android:layout_marginBottom="2dp"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="10sp" />
......@@ -209,8 +209,8 @@
android:id="@+id/iv_main_watching_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
android:layout_alignParentRight="true"
android:padding="6dp"
android:src="@mipmap/ic_quit_white"></ImageView>
<TextView
......@@ -219,8 +219,8 @@
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:background="@drawable/shape_red"
android:layout_marginRight="15dp"
android:background="@drawable/shape_red"
android:gravity="center"
android:paddingTop="8dp"
android:paddingBottom="10dp"
......
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<com.xinfu.helivideo.ui.widgets.NestedScrollableHost
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recy"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="30dp" />
</com.xinfu.helivideo.ui.widgets.NestedScrollableHost>
</FrameLayout>
\ No newline at end of file
......@@ -3,4 +3,3 @@ include ':oaid'
include ':rxpay'
include ':alipay'
include ':wxpay'
include ':videocache'
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
minSdkVersion 16
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
testImplementation 'junit:junit:4.12'
implementation 'org.conscrypt:conscrypt-android:2.2.1'
implementation("com.squareup.okhttp3:okhttp:4.4.0")
}
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
package com.danikula.videocache
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.danikula.videocache.test", appContext.packageName)
}
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.danikula.videocache" />
package com.danikula.videocache;
import java.io.ByteArrayInputStream;
import java.util.Arrays;
/**
* Simple memory based {@link Cache} implementation.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class ByteArrayCache implements Cache {
private volatile byte[] data;
private volatile boolean completed;
public ByteArrayCache() {
this(new byte[0]);
}
public ByteArrayCache(byte[] data) {
this.data = Preconditions.checkNotNull(data);
}
@Override
public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
if (offset >= data.length) {
return -1;
}
if (offset > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Too long offset for memory cache " + offset);
}
return new ByteArrayInputStream(data).read(buffer, (int) offset, length);
}
@Override
public long available() throws ProxyCacheException {
return data.length;
}
@Override
public void append(byte[] newData, int length) throws ProxyCacheException {
Preconditions.checkNotNull(data);
Preconditions.checkArgument(length >= 0 && length <= newData.length);
byte[] appendedData = Arrays.copyOf(data, data.length + length);
System.arraycopy(newData, 0, appendedData, data.length, length);
data = appendedData;
}
@Override
public void close() throws ProxyCacheException {
}
@Override
public void complete() {
completed = true;
}
@Override
public boolean isCompleted() {
return completed;
}
}
package com.danikula.videocache;
import java.io.ByteArrayInputStream;
/**
* Simple memory based {@link Source} implementation.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class ByteArraySource implements Source {
private final byte[] data;
private ByteArrayInputStream arrayInputStream;
public ByteArraySource(byte[] data) {
this.data = data;
}
@Override
public int read(byte[] buffer) throws ProxyCacheException {
return arrayInputStream.read(buffer, 0, buffer.length);
}
@Override
public long length() throws ProxyCacheException {
return data.length;
}
@Override
public void open(long offset) throws ProxyCacheException {
arrayInputStream = new ByteArrayInputStream(data);
arrayInputStream.skip(offset);
}
@Override
public void close() throws ProxyCacheException {
}
}
package com.danikula.videocache;
/**
* Cache for proxy.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public interface Cache {
long available() throws ProxyCacheException;
int read(byte[] buffer, long offset, int length) throws ProxyCacheException;
void append(byte[] data, int length) throws ProxyCacheException;
void close() throws ProxyCacheException;
void complete() throws ProxyCacheException;
boolean isCompleted();
}
package com.danikula.videocache;
import java.io.File;
/**
* Listener for cache availability.
*
* @author Egor Makovsky (yahor.makouski@gmail.com)
* @author Alexey Danilov (danikula@gmail.com).
*/
public interface CacheListener {
void onCacheAvailable(File cacheFile, String url, int percentsAvailable);
}
package com.danikula.videocache;
import com.danikula.videocache.file.DiskUsage;
import com.danikula.videocache.file.FileNameGenerator;
import com.danikula.videocache.headers.HeaderInjector;
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import java.io.File;
/**
* Configuration for proxy cache.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
class Config {
public final File cacheRoot;
public final FileNameGenerator fileNameGenerator;
public final DiskUsage diskUsage;
public final SourceInfoStorage sourceInfoStorage;
public final HeaderInjector headerInjector;
Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage, SourceInfoStorage sourceInfoStorage, HeaderInjector headerInjector) {
this.cacheRoot = cacheRoot;
this.fileNameGenerator = fileNameGenerator;
this.diskUsage = diskUsage;
this.sourceInfoStorage = sourceInfoStorage;
this.headerInjector = headerInjector;
}
File generateCacheFile(String url) {
String name = fileNameGenerator.generate(url);
return new File(cacheRoot, name);
}
}
package com.danikula.videocache;
import android.text.TextUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.danikula.videocache.Preconditions.checkNotNull;
/**
* Model for Http GET request.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
class GetRequest {
private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile("[R,r]ange:[ ]?bytes=(\\d*)-");
private static final Pattern URL_PATTERN = Pattern.compile("GET /(.*) HTTP");
public final String uri;
public final long rangeOffset;
public final boolean partial;
/**
* 预下载
*/
public boolean isPreLoad = false;
/**
* 预下载百分比
*/
public int percentsPreLoad = 4;
public GetRequest(String request) {
checkNotNull(request);
long offset = findRangeOffset(request);
this.rangeOffset = Math.max(0, offset);
this.partial = offset >= 0;
this.uri = findUri(request);
}
public static GetRequest read(InputStream inputStream) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
StringBuilder stringRequest = new StringBuilder();
String line;
while (!TextUtils.isEmpty(line = reader.readLine())) { // until new line (headers ending)
stringRequest.append(line).append('\n');
}
return new GetRequest(stringRequest.toString());
}
private long findRangeOffset(String request) {
Matcher matcher = RANGE_HEADER_PATTERN.matcher(request);
if (matcher.find()) {
String rangeValue = matcher.group(1);
return Long.parseLong(rangeValue);
}
return -1;
}
private String findUri(String request) {
Matcher matcher = URL_PATTERN.matcher(request);
if (matcher.find()) {
return matcher.group(1);
}
throw new IllegalArgumentException("Invalid request `" + request + "`: url not found!");
}
@Override
public String toString() {
return "GetRequest{" +
"rangeOffset=" + rangeOffset +
", partial=" + partial +
", uri='" + uri + '\'' +
'}';
}
}
package com.danikula.videocache;
import android.text.TextUtils;
import com.danikula.videocache.file.FileCache;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Locale;
import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
/**
* {@link ProxyCache} that read http url and writes data to {@link Socket}
*
* @author Alexey Danilov (danikula@gmail.com).
*/
class HttpProxyCache extends ProxyCache {
private static final float NO_CACHE_BARRIER = .2f;
protected final HttpUrlSource source;
protected final FileCache cache;
protected CacheListener listener;
public HttpProxyCache(HttpUrlSource source, FileCache cache) {
super(source, cache);
this.cache = cache;
this.source = source;
}
public void registerCacheListener(CacheListener cacheListener) {
this.listener = cacheListener;
}
public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException {
OutputStream out = new BufferedOutputStream(socket.getOutputStream());
String responseHeaders = newResponseHeaders(request);
out.write(responseHeaders.getBytes("UTF-8"));
long offset = request.rangeOffset;
if (isUseCache(request)) {
responseWithCache(out, offset);
} else {
responseWithoutCache(out, offset);
}
}
private boolean isUseCache(GetRequest request) throws ProxyCacheException {
long sourceLength = source.length();
boolean sourceLengthKnown = sourceLength > 0;
long cacheAvailable = cache.available();
// do not use cache for partial requests which too far from available cache. It seems user seek video.
return !sourceLengthKnown || !request.partial || request.rangeOffset <= cacheAvailable + sourceLength * NO_CACHE_BARRIER;
}
private String newResponseHeaders(GetRequest request) throws IOException, ProxyCacheException {
String mime = source.getMime();
boolean mimeKnown = !TextUtils.isEmpty(mime);
long length = cache.isCompleted() ? cache.available() : source.length();
boolean lengthKnown = length >= 0;
long contentLength = request.partial ? length - request.rangeOffset : length;
boolean addRange = lengthKnown && request.partial;
return new StringBuilder()
.append(request.partial ? "HTTP/1.1 206 PARTIAL CONTENT\n" : "HTTP/1.1 200 OK\n")
.append("Accept-Ranges: bytes\n")
.append(lengthKnown ? format("Content-Length: %d\n", contentLength) : "")
.append(addRange ? format("Content-Range: bytes %d-%d/%d\n", request.rangeOffset, length - 1, length) : "")
.append(mimeKnown ? format("Content-Type: %s\n", mime) : "")
.append("\n") // headers end
.toString();
}
private void responseWithCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
LogU.d("responseWithCache 请求带缓存数据"+offset);
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int readBytes;
while ((readBytes = read(buffer, offset, buffer.length)) != -1) {
out.write(buffer, 0, readBytes);
offset += readBytes;
LogU.d("responseWithCache 返回给播放器"+offset);
}
out.flush();
}
private void responseWithoutCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
LogU.d("responseWithCache 请求不带缓存数据"+offset);
HttpUrlSource newSourceNoCache = new HttpUrlSource(this.source);
try {
newSourceNoCache.open((int) offset);
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int readBytes;
while ((readBytes = newSourceNoCache.read(buffer)) != -1) {
LogU.d("responseWithCache 请求不带缓存数据"+offset+" readBytes "+readBytes);
out.write(buffer, 0, readBytes);
offset += readBytes;
}
out.flush();
} finally {
newSourceNoCache.close();
}
}
private String format(String pattern, Object... args) {
return String.format(Locale.US, pattern, args);
}
@Override
protected void onCachePercentsAvailableChanged(int percents) {
if (listener != null) {
listener.onCacheAvailable(cache.file, source.getUrl(), percents);
}
}
}
package com.danikula.videocache;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import com.danikula.videocache.file.FileCache;
import java.io.File;
import java.io.IOException;
import java.net.Socket;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import static com.danikula.videocache.Preconditions.checkNotNull;
/**
* Client for {@link HttpProxyCacheServer}
*
* @author Alexey Danilov (danikula@gmail.com).
*/
final class HttpProxyCacheServerClients {
private final AtomicInteger clientsCount = new AtomicInteger(0);
private final String url;
private volatile HttpProxyCache proxyCache;
private final List<CacheListener> listeners = new CopyOnWriteArrayList<>();
private final CacheListener uiCacheListener;
private final Config config;
public HttpProxyCacheServerClients(String url, Config config) {
this.url = checkNotNull(url);
this.config = checkNotNull(config);
this.uiCacheListener = new UiListenerHandler(url, listeners);
}
public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException {
startProcessRequest();
proxyCache.isPreLoad = request.isPreLoad;
proxyCache.setPercentsPreLoad( request.percentsPreLoad);
try {
clientsCount.incrementAndGet();
proxyCache.processRequest(request, socket);
} finally {
finishProcessRequest();
}
}
protected synchronized HttpProxyCache startProcessRequest() throws ProxyCacheException {
proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache;
return proxyCache;
}
private synchronized void finishProcessRequest() {
if (clientsCount.decrementAndGet() <= 0) {
proxyCache.shutdown();
proxyCache = null;
}
}
public void registerCacheListener(CacheListener cacheListener) {
listeners.add(cacheListener);
}
public void unregisterCacheListener(CacheListener cacheListener) {
listeners.remove(cacheListener);
}
public void shutdown() {
listeners.clear();
if (proxyCache != null) {
proxyCache.registerCacheListener(null);
proxyCache.shutdown();
proxyCache = null;
}
clientsCount.set(0);
}
public int getClientsCount() {
return clientsCount.get();
}
private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
HttpUrlSource source = new HttpUrlSource(url, config.sourceInfoStorage, config.headerInjector);
FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage);
HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache);
httpProxyCache.registerCacheListener(uiCacheListener);
return httpProxyCache;
}
private static final class UiListenerHandler extends Handler implements CacheListener {
private final String url;
private final List<CacheListener> listeners;
public UiListenerHandler(String url, List<CacheListener> listeners) {
super(Looper.getMainLooper());
this.url = url;
this.listeners = listeners;
}
@Override
public void onCacheAvailable(File file, String url, int percentsAvailable) {
Message message = obtainMessage();
message.arg1 = percentsAvailable;
message.obj = file;
sendMessage(message);
}
@Override
public void handleMessage(Message msg) {
for (CacheListener cacheListener : listeners) {
cacheListener.onCacheAvailable((File) msg.obj, url, msg.arg1);
}
}
}
}
package com.danikula.videocache;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import okhttp3.Request;
import okhttp3.Response;
public class HttpProxyPreLoader {
private static String TAG = "HttpProxyPreLoader";
protected ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
static final String preUrlPx = "_HttpProxyPre";
private Map<String, PreRunnable> runnableMap = new HashMap<>();
public boolean isLoading(String url) {
return runnableMap.get(url) != null;
}
public void startLoad(String url, int percentsPreLoad, long totalLen) {
LogU.d("开始预下载 total " + url);
PreRunnable runnable = new PreRunnable(url, percentsPreLoad,totalLen);
runnableMap.put(url, runnable);
cachedThreadPool.execute(runnable);
}
public int getPercentsPreLoad(String url) {
PreRunnable runnable = runnableMap.get(url);
if (runnable == null) {
return 0;
} else {
return runnable.percentsPreLoad;
}
}
public void stopLoad(String url) {
PreRunnable runnable = runnableMap.get(url);
if (runnable != null) {
runnable.stop = true;
LogU.d("停止预下载 total " + url);
}
}
class PreRunnable implements Runnable {
private String url;
private int percentsPreLoad;
protected boolean stop = false;
private long len;
public PreRunnable(String url, int percentsPreLoad, Long total) {
this.url = url;
this.len = total;
this.percentsPreLoad = percentsPreLoad;
}
@Override
public void run() {
final Request request = new Request.Builder()
.url(url)
.head()
.build();
try {
// Response response = OkManager.getInstance().client.newCall(request).execute();
// long length = Long.parseLong(response.header("content-length"));//获取文件长度
// long targetLen = length * (percentsPreLoad / 100L);
int targetLen = (int) (len * (percentsPreLoad / 100.0));
;//length * (percentsPreLoad / 100L);
Request requestLoad = new Request.Builder()
.url(url)
.addHeader("Range", String.format("bytes=%d-%d", 0, targetLen))
.build();
Response responseLoad = OkManager.getInstance().client.newCall(requestLoad).execute();
InputStream inputStream = responseLoad.body().byteStream();//获取流
final long M = 1024;
byte[] bytes = new byte[(int) M];
long seek = 0;
long total = 0;
for (; ; ) {
int readCount = inputStream.read(bytes);
total += readCount;
seek += readCount;
LogU.d("预下载客户端 total " + total+" 应该下载 "+targetLen +" readCount " +readCount);
if (readCount == 0) {
continue;
}
if (total > targetLen) {
break;
}
if (readCount == -1) {
inputStream.close();
break;
}
if (stop) {
inputStream.close();
break;
}
}
responseLoad.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
runnableMap.remove(url);
}
}
}
}
package com.danikula.videocache;
import android.text.TextUtils;
import com.danikula.videocache.headers.EmptyHeadersInjector;
import com.danikula.videocache.headers.HeaderInjector;
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
import static com.danikula.videocache.Preconditions.checkNotNull;
import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.net.HttpURLConnection.HTTP_PARTIAL;
import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
/**
* {@link Source} that uses http resource as source for {@link ProxyCache}.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class HttpUrlSource implements Source {
private static final int MAX_REDIRECTS = 5;
private final SourceInfoStorage sourceInfoStorage;
private final HeaderInjector headerInjector;
private SourceInfo sourceInfo;
private HttpURLConnection connection;
private InputStream inputStream;
public HttpUrlSource(String url) {
this(url, SourceInfoStorageFactory.newEmptySourceInfoStorage());
}
public HttpUrlSource(String url, SourceInfoStorage sourceInfoStorage) {
this(url, sourceInfoStorage, new EmptyHeadersInjector());
}
public HttpUrlSource(String url, SourceInfoStorage sourceInfoStorage, HeaderInjector headerInjector) {
this.sourceInfoStorage = checkNotNull(sourceInfoStorage);
this.headerInjector = checkNotNull(headerInjector);
SourceInfo sourceInfo = sourceInfoStorage.get(url);
this.sourceInfo = sourceInfo != null ? sourceInfo :
new SourceInfo(url, Integer.MIN_VALUE, ProxyCacheUtils.getSupposablyMime(url));
}
public HttpUrlSource(HttpUrlSource source) {
this.sourceInfo = source.sourceInfo;
this.sourceInfoStorage = source.sourceInfoStorage;
this.headerInjector = source.headerInjector;
}
@Override
public synchronized long length() throws ProxyCacheException {
if (sourceInfo.length == Integer.MIN_VALUE) {
fetchContentInfo();
}
return sourceInfo.length;
}
@Override
public void open(long offset) throws ProxyCacheException {
try {
connection = openConnection(offset, -1);
String mime = connection.getContentType();
inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE);
long length = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
} catch (IOException e) {
throw new ProxyCacheException("Error opening connection for " + sourceInfo.url + " with offset " + offset, e);
}
}
private long readSourceAvailableBytes(HttpURLConnection connection, long offset, int responseCode) throws IOException {
long contentLength = getContentLength(connection);
return responseCode == HTTP_OK ? contentLength
: responseCode == HTTP_PARTIAL ? contentLength + offset : sourceInfo.length;
}
private long getContentLength(HttpURLConnection connection) {
String contentLengthValue = connection.getHeaderField("Content-Length");
return contentLengthValue == null ? -1 : Long.parseLong(contentLengthValue);
}
@Override
public void close() throws ProxyCacheException {
if (connection != null) {
try {
connection.disconnect();
} catch (NullPointerException | IllegalArgumentException e) {
String message = "Wait... but why? WTF!? " +
"Really shouldn't happen any more after fixing https://github.com/danikula/AndroidVideoCache/issues/43. " +
"If you read it on your device log, please, notify me danikula@gmail.com or create issue here " +
"https://github.com/danikula/AndroidVideoCache/issues.";
throw new RuntimeException(message, e);
} catch (ArrayIndexOutOfBoundsException e) {
LogU.e("Error closing connection correctly. Should happen only on Android L. " +
"If anybody know how to fix it, please visit https://github.com/danikula/AndroidVideoCache/issues/88. " +
"Until good solution is not know, just ignore this issue :(",e);
}
}
}
@Override
public int read(byte[] buffer) throws ProxyCacheException {
if (inputStream == null) {
throw new ProxyCacheException("Error reading data from " + sourceInfo.url + ": connection is absent!");
}
try {
return inputStream.read(buffer, 0, buffer.length);
} catch (InterruptedIOException e) {
throw new InterruptedProxyCacheException("Reading source " + sourceInfo.url + " is interrupted", e);
} catch (IOException e) {
throw new ProxyCacheException("Error reading data from " + sourceInfo.url, e);
}
}
private void fetchContentInfo() throws ProxyCacheException {
LogU.d("Read content info from " + sourceInfo.url);
HttpURLConnection urlConnection = null;
InputStream inputStream = null;
try {
urlConnection = openConnection(0, 10000);
long length = getContentLength(urlConnection);
String mime = urlConnection.getContentType();
inputStream = urlConnection.getInputStream();
this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
LogU.d("Source info fetched: " + sourceInfo);
} catch (IOException e) {
LogU.e("Error fetching info from " + sourceInfo.url, e);
} finally {
ProxyCacheUtils.close(inputStream);
if (urlConnection != null) {
urlConnection.disconnect();
}
}
}
private HttpURLConnection openConnection(long offset, int timeout) throws IOException, ProxyCacheException {
HttpURLConnection connection;
boolean redirected;
int redirectCount = 0;
String url = this.sourceInfo.url;
do {
LogU.d("Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
connection = (HttpURLConnection) new URL(url).openConnection();
injectCustomHeaders(connection, url);
if (offset > 0) {
connection.setRequestProperty("Range", "bytes=" + offset + "-");
}
if (timeout > 0) {
connection.setConnectTimeout(timeout);
connection.setReadTimeout(timeout);
}
int code = connection.getResponseCode();
redirected = code == HTTP_MOVED_PERM || code == HTTP_MOVED_TEMP || code == HTTP_SEE_OTHER;
if (redirected) {
url = connection.getHeaderField("Location");
redirectCount++;
connection.disconnect();
}
if (redirectCount > MAX_REDIRECTS) {
throw new ProxyCacheException("Too many redirects: " + redirectCount);
}
} while (redirected);
return connection;
}
private void injectCustomHeaders(HttpURLConnection connection, String url) {
Map<String, String> extraHeaders = headerInjector.addHeaders(url);
for (Map.Entry<String, String> header : extraHeaders.entrySet()) {
connection.setRequestProperty(header.getKey(), header.getValue());
}
}
public synchronized String getMime() throws ProxyCacheException {
if (TextUtils.isEmpty(sourceInfo.mime)) {
fetchContentInfo();
}
return sourceInfo.mime;
}
public String getUrl() {
return sourceInfo.url;
}
@Override
public String toString() {
return "HttpUrlSource{sourceInfo='" + sourceInfo + "}";
}
}
package com.danikula.videocache;
import java.io.IOException;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import static com.danikula.videocache.Preconditions.checkNotNull;
/**
* {@link ProxySelector} that ignore system default proxies for concrete host.
* <p>
* It is important to <a href="https://github.com/danikula/AndroidVideoCache/issues/28">ignore system proxy</a> for localhost connection.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
class IgnoreHostProxySelector extends ProxySelector {
private static final List<Proxy> NO_PROXY_LIST = Arrays.asList(Proxy.NO_PROXY);
private final ProxySelector defaultProxySelector;
private final String hostToIgnore;
private final int portToIgnore;
IgnoreHostProxySelector(ProxySelector defaultProxySelector, String hostToIgnore, int portToIgnore) {
this.defaultProxySelector = checkNotNull(defaultProxySelector);
this.hostToIgnore = checkNotNull(hostToIgnore);
this.portToIgnore = portToIgnore;
}
static void install(String hostToIgnore, int portToIgnore) {
ProxySelector defaultProxySelector = ProxySelector.getDefault();
ProxySelector ignoreHostProxySelector = new IgnoreHostProxySelector(defaultProxySelector, hostToIgnore, portToIgnore);
ProxySelector.setDefault(ignoreHostProxySelector);
}
@Override
public List<Proxy> select(URI uri) {
boolean ignored = hostToIgnore.equals(uri.getHost()) && portToIgnore == uri.getPort();
return ignored ? NO_PROXY_LIST : defaultProxySelector.select(uri);
}
@Override
public void connectFailed(URI uri, SocketAddress address, IOException failure) {
defaultProxySelector.connectFailed(uri, address, failure);
}
}
package com.danikula.videocache;
/**
* Indicates interruption error in work of {@link ProxyCache} fired by user.
*
* @author Alexey Danilov
*/
public class InterruptedProxyCacheException extends ProxyCacheException {
public InterruptedProxyCacheException(String message) {
super(message);
}
public InterruptedProxyCacheException(String message, Throwable cause) {
super(message, cause);
}
public InterruptedProxyCacheException(Throwable cause) {
super(cause);
}
}
package com.danikula.videocache;
import android.util.Log;
public class LogU {
public static void d(String msg){
Log.d("hapivideocache",msg);
}
public static void d(String msg,Throwable e){
Log.d("hapivideocache",msg+e.getMessage()); }
public static void e(String msg,Throwable e){
Log.e("hapivideocache",msg+e.getMessage()); }
public static void e(String msg){
Log.e("hapivideocache",msg); }
}
package com.danikula.videocache;
import okhttp3.OkHttpClient;
public class OkManager {
protected OkHttpClient client; // = createOkHttpClient();
private OkManager(){
client = new OkHttpClient();
}
public static OkManager getInstance(){
return Holder.ok;
}
private static class Holder{
public static OkManager ok = new OkManager();
}
}
package com.danikula.videocache;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import static com.danikula.videocache.Preconditions.checkArgument;
import static com.danikula.videocache.Preconditions.checkNotNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* Pings {@link HttpProxyCacheServer} to make sure it works.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
class Pinger {
private static final String PING_REQUEST = "ping";
private static final String PING_RESPONSE = "ping ok";
private final ExecutorService pingExecutor = Executors.newSingleThreadExecutor();
private final String host;
private final int port;
Pinger(String host, int port) {
this.host = checkNotNull(host);
this.port = port;
}
boolean ping(int maxAttempts, int startTimeout) {
checkArgument(maxAttempts >= 1);
checkArgument(startTimeout > 0);
int timeout = startTimeout;
int attempts = 0;
while (attempts < maxAttempts) {
try {
Future<Boolean> pingFuture = pingExecutor.submit(new PingCallable());
boolean pinged = pingFuture.get(timeout, MILLISECONDS);
if (pinged) {
return true;
}
} catch (TimeoutException e) {
LogU.d("Error pinging server (attempt: " + attempts + ", timeout: " + timeout + "). ");
} catch (InterruptedException | ExecutionException e) {
LogU.e("Error pinging server due to unexpected error", e);
}
attempts++;
timeout *= 2;
}
String error = String.format(Locale.US, "Error pinging server (attempts: %d, max timeout: %d). " +
"If you see this message, please, report at https://github.com/danikula/AndroidVideoCache/issues/134. " +
"Default proxies are: %s"
, attempts, timeout / 2, getDefaultProxies());
LogU.e(error, new ProxyCacheException(error));
return false;
}
private List<Proxy> getDefaultProxies() {
try {
ProxySelector defaultProxySelector = ProxySelector.getDefault();
return defaultProxySelector.select(new URI(getPingUrl()));
} catch (URISyntaxException e) {
throw new IllegalStateException(e);
}
}
boolean isPingRequest(String request) {
return PING_REQUEST.equals(request);
}
void responseToPing(Socket socket) throws IOException {
OutputStream out = socket.getOutputStream();
out.write("HTTP/1.1 200 OK\n\n".getBytes());
out.write(PING_RESPONSE.getBytes());
}
private boolean pingServer() throws ProxyCacheException {
String pingUrl = getPingUrl();
HttpUrlSource source = new HttpUrlSource(pingUrl);
try {
byte[] expectedResponse = PING_RESPONSE.getBytes();
source.open(0);
byte[] response = new byte[expectedResponse.length];
source.read(response);
boolean pingOk = Arrays.equals(expectedResponse, response);
LogU.d("Ping response: `" + new String(response) + "`, pinged? " + pingOk);
return pingOk;
} catch (ProxyCacheException e) {
LogU.e("Error reading ping response", e);
return false;
} finally {
source.close();
}
}
private String getPingUrl() {
return String.format(Locale.US, "http://%s:%d/%s", host, port, PING_REQUEST);
}
private class PingCallable implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
return pingServer();
}
}
}
package com.danikula.videocache;
public final class Preconditions {
public static <T> T checkNotNull(T reference) {
if (reference == null) {
throw new NullPointerException();
}
return reference;
}
public static void checkAllNotNull(Object... references) {
for (Object reference : references) {
if (reference == null) {
throw new NullPointerException();
}
}
}
public static <T> T checkNotNull(T reference, String errorMessage) {
if (reference == null) {
throw new NullPointerException(errorMessage);
}
return reference;
}
static void checkArgument(boolean expression) {
if (!expression) {
throw new IllegalArgumentException();
}
}
static void checkArgument(boolean expression, String errorMessage) {
if (!expression) {
throw new IllegalArgumentException(errorMessage);
}
}
}
package com.danikula.videocache;
import java.util.concurrent.atomic.AtomicInteger;
import static com.danikula.videocache.Preconditions.checkNotNull;
/**
* Proxy for {@link Source} with caching support ({@link Cache}).
* <p/>
* Can be used only for sources with persistent data (that doesn't change with time).
* Method {@link #read(byte[], long, int)} will be blocked while fetching data from source.
* Useful for streaming something with caching e.g. streaming video/audio etc.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
class ProxyCache {
private static final int MAX_READ_SOURCE_ATTEMPTS = 1;
private final Source source;
private final Cache cache;
private final Object wc = new Object();
private final Object stopLock = new Object();
private final AtomicInteger readSourceErrorsCount;
private volatile Thread sourceReaderThread;
private volatile boolean stopped;
private volatile int percentsAvailable = -1;
/**
* 预下载
*/
public boolean isPreLoad = false;
/**
* 预下载百分比
*/
private int percentsPreLoad = 4;
public void setPercentsPreLoad(int percentsPreLoad){
this.percentsPreLoad=percentsPreLoad;
}
public ProxyCache(Source source, Cache cache) {
this.source = checkNotNull(source);
this.cache = checkNotNull(cache);
this.readSourceErrorsCount = new AtomicInteger();
}
public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
ProxyCacheUtils.assertBuffer(buffer, offset, length);
Boolean isCompleted = cache.isCompleted();
Long available = cache.available();
while (!isCompleted && available < (offset + length) && !stopped) {
LogU.d("请求读写 isCompleted " + isCompleted + " available " + available + " (offset + length) " + (offset + length) + " length " + length + " stopped " + stopped + " 是否读好了 " + (!isCompleted && available < (offset + length) && !stopped));
readSourceAsync();
waitForSourceData();
checkReadSourceErrorsCount();
isCompleted = cache.isCompleted();
available = cache.available();
}
int read = cache.read(buffer, offset, length);
if (cache.isCompleted() && percentsAvailable != 100) {
percentsAvailable = 100;
onCachePercentsAvailableChanged(100);
}
LogU.d("请求读写 返回读写字节");
return read;
}
private void checkReadSourceErrorsCount() throws ProxyCacheException {
int errorsCount = readSourceErrorsCount.get();
if (errorsCount >= MAX_READ_SOURCE_ATTEMPTS) {
readSourceErrorsCount.set(0);
throw new ProxyCacheException("Error reading source " + errorsCount + " times");
}
}
public void shutdown() {
synchronized (stopLock) {
LogU.d("Shutdown proxy for " + source);
try {
stopped = true;
if (sourceReaderThread != null) {
sourceReaderThread.interrupt();
}
cache.close();
} catch (ProxyCacheException e) {
onError(e);
}
}
}
private synchronized void readSourceAsync() throws ProxyCacheException {
boolean readingInProgress = sourceReaderThread != null && sourceReaderThread.getState() != Thread.State.TERMINATED;
if (!stopped && !cache.isCompleted() && !readingInProgress) {
sourceReaderThread = new Thread(new SourceReaderRunnable(), "Source reader for " + source);
sourceReaderThread.start();
}
}
private void waitForSourceData() throws ProxyCacheException {
synchronized (wc) {
try {
wc.wait(1000);
} catch (InterruptedException e) {
throw new ProxyCacheException("Waiting source data is interrupted!", e);
}
}
}
private void notifyNewCacheDataAvailable(long cacheAvailable, long sourceAvailable) {
onCacheAvailable(cacheAvailable, sourceAvailable);
synchronized (wc) {
wc.notifyAll();
}
}
protected void onCacheAvailable(long cacheAvailable, long sourceLength) {
boolean zeroLengthSource = sourceLength == 0;
int percents = zeroLengthSource ? 100 : (int) ((float) cacheAvailable / sourceLength * 100);
boolean percentsChanged = percents != percentsAvailable;
boolean sourceLengthKnown = sourceLength >= 0;
if (sourceLengthKnown && percentsChanged) {
onCachePercentsAvailableChanged(percents);
}
percentsAvailable = percents;
}
protected void onCachePercentsAvailableChanged(int percentsAvailable) {
}
private void readSource() {
long sourceAvailable = -1;
long offset = 0;
try {
offset = cache.available();
source.open(offset);
sourceAvailable = source.length();
byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
int readBytes;
boolean breakPre = true;
while (breakPre && (readBytes = source.read(buffer)) != -1) {
synchronized (stopLock) {
if (isStopped()) {
return;
}
cache.append(buffer, readBytes);
}
offset += readBytes;
notifyNewCacheDataAvailable(offset, sourceAvailable);
LogU.d("读取网络" + readBytes);
LogU.d("offset " + offset);
LogU.d("percentsAvailable " + percentsAvailable + " percentsPreLoad " + percentsPreLoad);
if (isPreLoad) {
if (percentsAvailable >= percentsPreLoad) {
LogU.d("offset 超过了不缓存" + offset);
breakPre = false;
break;
}
}
}
tryComplete();
onSourceRead();
} catch (Throwable e) {
readSourceErrorsCount.incrementAndGet();
LogU.d("读取网络出错");
onError(e);
} finally {
LogU.d("读取网络完成");
closeSource();
notifyNewCacheDataAvailable(offset, sourceAvailable);
}
}
private void onSourceRead() {
// guaranteed notify listeners after source read and cache completed
percentsAvailable = 100;
onCachePercentsAvailableChanged(percentsAvailable);
}
private void tryComplete() throws ProxyCacheException {
synchronized (stopLock) {
if (!isStopped() && cache.available() == source.length()) {
cache.complete();
}
}
}
private boolean isStopped() {
return Thread.currentThread().isInterrupted() || stopped;
}
private void closeSource() {
try {
source.close();
} catch (ProxyCacheException e) {
onError(new ProxyCacheException("Error closing source " + source, e));
}
}
protected final void onError(final Throwable e) {
boolean interruption = e instanceof InterruptedProxyCacheException;
if (interruption) {
LogU.d("ProxyCache is interrupted");
} else {
LogU.e("ProxyCache error", e);
}
}
private class SourceReaderRunnable implements Runnable {
@Override
public void run() {
readSource();
}
}
}
package com.danikula.videocache;
/**
* Indicates any error in work of {@link ProxyCache}.
*
* @author Alexey Danilov
*/
public class ProxyCacheException extends Exception {
private static final String LIBRARY_VERSION = ". Version: " + "1.0.0";
public ProxyCacheException(String message) {
super(message + LIBRARY_VERSION);
}
public ProxyCacheException(String message, Throwable cause) {
super(message + LIBRARY_VERSION, cause);
}
public ProxyCacheException(Throwable cause) {
super("No explanation error" + LIBRARY_VERSION, cause);
}
}
package com.danikula.videocache;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import java.io.Closeable;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import static com.danikula.videocache.Preconditions.checkArgument;
import static com.danikula.videocache.Preconditions.checkNotNull;
/**
* Just simple utils.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class ProxyCacheUtils {
static final int DEFAULT_BUFFER_SIZE = 8 * 1024;
static final int MAX_ARRAY_PREVIEW = 16;
static String getSupposablyMime(String url) {
MimeTypeMap mimes = MimeTypeMap.getSingleton();
String extension = MimeTypeMap.getFileExtensionFromUrl(url);
return TextUtils.isEmpty(extension) ? null : mimes.getMimeTypeFromExtension(extension);
}
static void assertBuffer(byte[] buffer, long offset, int length) {
checkNotNull(buffer, "Buffer must be not null!");
checkArgument(offset >= 0, "Data offset must be positive!");
checkArgument(length >= 0 && length <= buffer.length, "Length must be in range [0..buffer.length]");
}
static String preview(byte[] data, int length) {
int previewLength = Math.min(MAX_ARRAY_PREVIEW, Math.max(length, 0));
byte[] dataRange = Arrays.copyOfRange(data, 0, previewLength);
String preview = Arrays.toString(dataRange);
if (previewLength < length) {
preview = preview.substring(0, preview.length() - 1) + ", ...]";
}
return preview;
}
static String encode(String url) {
try {
return URLEncoder.encode(url, "utf-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Error encoding url", e);
}
}
static String decode(String url) {
try {
return URLDecoder.decode(url, "utf-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Error decoding url", e);
}
}
static void close(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
LogU.e("Error closing resource", e);
}
}
}
public static String computeMD5(String string) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] digestBytes = messageDigest.digest(string.getBytes());
return bytesToHexString(digestBytes);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
private static String bytesToHexString(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
package com.danikula.videocache;
/**
* Source for proxy.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public interface Source {
/**
* Opens source. Source should be open before using {@link #read(byte[])}
*
* @param offset offset in bytes for source.
* @throws ProxyCacheException if error occur while opening source.
*/
void open(long offset) throws ProxyCacheException;
/**
* Returns length bytes or <b>negative value</b> if length is unknown.
*
* @return bytes length
* @throws ProxyCacheException if error occur while fetching source data.
*/
long length() throws ProxyCacheException;
/**
* Read data to byte buffer from source with current offset.
*
* @param buffer a buffer to be used for reading data.
* @return a count of read bytes
* @throws ProxyCacheException if error occur while reading source.
*/
int read(byte[] buffer) throws ProxyCacheException;
/**
* Closes source and release resources. Every opened source should be closed.
*
* @throws ProxyCacheException if error occur while closing source.
*/
void close() throws ProxyCacheException;
}
package com.danikula.videocache;
/**
* Stores source's info.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class SourceInfo {
public final String url;
public final long length;
public final String mime;
public SourceInfo(String url, long length, String mime) {
this.url = url;
this.length = length;
this.mime = mime;
}
@Override
public String toString() {
return "SourceInfo{" +
"url='" + url + '\'' +
", length=" + length +
", mime='" + mime + '\'' +
'}';
}
}
package com.danikula.videocache;
import android.content.Context;
import android.os.Environment;
import java.io.File;
import static android.os.Environment.MEDIA_MOUNTED;
/**
* Provides application storage paths
* <p/>
* See https://github.com/nostra13/Android-Universal-Image-Loader
*
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
* @since 1.0.0
*/
final class StorageUtils {
private static final String INDIVIDUAL_DIR_NAME = "video-cache";
/**
* Returns individual application cache directory (for only video caching from Proxy). Cache directory will be
* created on SD card <i>("/Android/data/[app_package_name]/cache/video-cache")</i> if card is mounted .
* Else - Android defines cache directory on device's file system.
*
* @param context Application context
* @return Cache {@link File directory}
*/
public static File getIndividualCacheDirectory(Context context) {
File cacheDir = getCacheDirectory(context, true);
return new File(cacheDir, INDIVIDUAL_DIR_NAME);
}
/**
* Returns application cache directory. Cache directory will be created on SD card
* <i>("/Android/data/[app_package_name]/cache")</i> (if card is mounted and app has appropriate permission) or
* on device's file system depending incoming parameters.
*
* @param context Application context
* @param preferExternal Whether prefer external location for cache
* @return Cache {@link File directory}.<br />
* <b>NOTE:</b> Can be null in some unpredictable cases (if SD card is unmounted and
* {@link Context#getCacheDir() Context.getCacheDir()} returns null).
*/
private static File getCacheDirectory(Context context, boolean preferExternal) {
File appCacheDir = null;
String externalStorageState;
try {
externalStorageState = Environment.getExternalStorageState();
} catch (NullPointerException e) { // (sh)it happens
externalStorageState = "";
}
if (preferExternal && MEDIA_MOUNTED.equals(externalStorageState)) {
appCacheDir = getExternalCacheDir(context);
}
if (appCacheDir == null) {
appCacheDir = context.getCacheDir();
}
if (appCacheDir == null) {
String cacheDirPath = "/data/data/" + context.getPackageName() + "/cache/";
LogU.d("Can't define system cache directory! '" + cacheDirPath + "%s' will be used.");
appCacheDir = new File(cacheDirPath);
}
return appCacheDir;
}
private static File getExternalCacheDir(Context context) {
File dataDir = new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data");
File appCacheDir = new File(new File(dataDir, context.getPackageName()), "cache");
if (!appCacheDir.exists()) {
if (!appCacheDir.mkdirs()) {
LogU.d("Unable to create external cache directory");
return null;
}
}
return appCacheDir;
}
}
package com.danikula.videocache.file;
import java.io.File;
import java.io.IOException;
/**
* Declares how {@link FileCache} will use disc space.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public interface DiskUsage {
void touch(File file) throws IOException;
}
package com.danikula.videocache.file;
import com.danikula.videocache.Cache;
import com.danikula.videocache.LogU;
import com.danikula.videocache.ProxyCacheException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
/**
* {@link Cache} that uses file for storing data.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class FileCache implements Cache {
private static final String TEMP_POSTFIX = ".download";
private final DiskUsage diskUsage;
public File file;
private RandomAccessFile dataFile;
public FileCache(File file) throws ProxyCacheException {
this(file, new UnlimitedDiskUsage());
}
public FileCache(File file, DiskUsage diskUsage) throws ProxyCacheException {
try {
if (diskUsage == null) {
throw new NullPointerException();
}
this.diskUsage = diskUsage;
File directory = file.getParentFile();
Files.makeDir(directory);
boolean completed = file.exists();
this.file = completed ? file : new File(file.getParentFile(), file.getName() + TEMP_POSTFIX);
this.dataFile = new RandomAccessFile(this.file, completed ? "r" : "rw");
long size = this.file.length();
LogU.d("临时文件大小"+size+" dataFile "+dataFile.length()+"file path"+file.getAbsolutePath());
} catch (IOException e) {
throw new ProxyCacheException("Error using file " + file + " as disc cache", e);
}
}
@Override
public synchronized long available() throws ProxyCacheException {
try {
return (int) dataFile.length();
} catch (IOException e) {
throw new ProxyCacheException("Error reading length of file " + file, e);
}
}
@Override
public synchronized int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
try {
dataFile.seek(offset);
LogU.d(":从文件缓存里读 "+offset+" "+length);
return dataFile.read(buffer, 0, length);
} catch (IOException e) {
String format = "Error reading %d bytes with offset %d from file[%d bytes] to buffer[%d bytes]";
throw new ProxyCacheException(String.format(format, length, offset, available(), buffer.length), e);
}
}
@Override
public synchronized void append(byte[] data, int length) throws ProxyCacheException {
try {
if (isCompleted()) {
throw new ProxyCacheException("Error append cache: cache file " + file + " is completed!");
}
dataFile.seek(available());
dataFile.write(data, 0, length);
} catch (IOException e) {
String format = "Error writing %d bytes to %s from buffer with size %d";
throw new ProxyCacheException(String.format(format, length, dataFile, data.length), e);
}
}
@Override
public synchronized void close() throws ProxyCacheException {
try {
dataFile.close();
diskUsage.touch(file);
} catch (IOException e) {
throw new ProxyCacheException("Error closing file " + file, e);
}
}
@Override
public synchronized void complete() throws ProxyCacheException {
if (isCompleted()) {
return;
}
close();
String fileName = file.getName().substring(0, file.getName().length() - TEMP_POSTFIX.length());
File completedFile = new File(file.getParentFile(), fileName);
boolean renamed = file.renameTo(completedFile);
if (!renamed) {
throw new ProxyCacheException("Error renaming file " + file + " to " + completedFile + " for completion!");
}
file = completedFile;
try {
dataFile = new RandomAccessFile(file, "r");
diskUsage.touch(file);
} catch (IOException e) {
throw new ProxyCacheException("Error opening " + file + " as disc cache", e);
}
}
@Override
public synchronized boolean isCompleted() {
return !isTempFile(file);
}
/**
* Returns file to be used fo caching. It may as original file passed in constructor as some temp file for not completed cache.
*
* @return file for caching.
*/
public File getFile() {
return file;
}
private boolean isTempFile(File file) {
return file.getName().endsWith(TEMP_POSTFIX);
}
}
package com.danikula.videocache.file;
/**
* Generator for files to be used for caching.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public interface FileNameGenerator {
String generate(String url);
}
package com.danikula.videocache.file;
import com.danikula.videocache.LogU;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
/**
* Utils for work with files.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
class Files {
static void makeDir(File directory) throws IOException {
if (directory.exists()) {
if (!directory.isDirectory()) {
throw new IOException("File " + directory + " is not directory!");
}
} else {
boolean isCreated = directory.mkdirs();
if (!isCreated) {
throw new IOException(String.format("Directory %s can't be created", directory.getAbsolutePath()));
}
}
}
static List<File> getLruListFiles(File directory) {
List<File> result = new LinkedList<>();
File[] files = directory.listFiles();
if (files != null) {
result = Arrays.asList(files);
Collections.sort(result, new LastModifiedComparator());
}
return result;
}
static void setLastModifiedNow(File file) throws IOException {
if (file.exists()) {
long now = System.currentTimeMillis();
boolean modified = file.setLastModified(now); // on some devices (e.g. Nexus 5) doesn't work
if (!modified) {
modify(file);
if (file.lastModified() < now) {
// NOTE: apparently this is a known issue (see: http://stackoverflow.com/questions/6633748/file-lastmodified-is-never-what-was-set-with-file-setlastmodified)
LogU.d("Last modified date {} is not set for file {}");
}
}
}
}
static void modify(File file) throws IOException {
long size = file.length();
if (size == 0) {
recreateZeroSizeFile(file);
return;
}
RandomAccessFile accessFile = new RandomAccessFile(file, "rwd");
accessFile.seek(size - 1);
byte lastByte = accessFile.readByte();
accessFile.seek(size - 1);
accessFile.write(lastByte);
accessFile.close();
}
private static void recreateZeroSizeFile(File file) throws IOException {
if (!file.delete() || !file.createNewFile()) {
throw new IOException("Error recreate zero-size file " + file);
}
}
private static final class LastModifiedComparator implements Comparator<File> {
@Override
public int compare(File lhs, File rhs) {
return compareLong(lhs.lastModified(), rhs.lastModified());
}
private int compareLong(long first, long second) {
return (first < second) ? -1 : ((first == second) ? 0 : 1);
}
}
}
package com.danikula.videocache.file;
import com.danikula.videocache.LogU;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* {@link DiskUsage} that uses LRU (Least Recently Used) strategy to trim cache.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public abstract class LruDiskUsage implements DiskUsage {
private final ExecutorService workerThread = Executors.newSingleThreadExecutor();
@Override
public void touch(File file) throws IOException {
workerThread.submit(new TouchCallable(file));
}
private void touchInBackground(File file) throws IOException {
Files.setLastModifiedNow(file);
List<File> files = Files.getLruListFiles(file.getParentFile());
trim(files);
}
protected abstract boolean accept(File file, long totalSize, int totalCount);
private void trim(List<File> files) {
long totalSize = countTotalSize(files);
int totalCount = files.size();
for (File file : files) {
boolean accepted = accept(file, totalSize, totalCount);
if (!accepted) {
long fileSize = file.length();
boolean deleted = file.delete();
if (deleted) {
totalCount--;
totalSize -= fileSize;
LogU.d("Cache file " + file + " is deleted because it exceeds cache limit");
} else {
LogU.e("Error deleting file " + file + " for trimming cache");
}
}
}
}
private long countTotalSize(List<File> files) {
long totalSize = 0;
for (File file : files) {
totalSize += file.length();
}
return totalSize;
}
private class TouchCallable implements Callable<Void> {
private final File file;
public TouchCallable(File file) {
this.file = file;
}
@Override
public Void call() throws Exception {
touchInBackground(file);
return null;
}
}
}
package com.danikula.videocache.file;
import android.text.TextUtils;
import com.danikula.videocache.ProxyCacheUtils;
/**
* Implementation of {@link FileNameGenerator} that uses MD5 of url as file name
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class Md5FileNameGenerator implements FileNameGenerator {
private static final int MAX_EXTENSION_LENGTH = 4;
@Override
public String generate(String url) {
String extension = getExtension(url);
String name = ProxyCacheUtils.computeMD5(url);
return TextUtils.isEmpty(extension) ? name : name + "." + extension;
}
private String getExtension(String url) {
int dotIndex = url.lastIndexOf('.');
int slashIndex = url.lastIndexOf('/');
return dotIndex != -1 && dotIndex > slashIndex && dotIndex + 2 + MAX_EXTENSION_LENGTH > url.length() ?
url.substring(dotIndex + 1, url.length()) : "";
}
}
package com.danikula.videocache.file;
import java.io.File;
/**
* {@link DiskUsage} that uses LRU (Least Recently Used) strategy and trims cache size to max files count if needed.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class TotalCountLruDiskUsage extends LruDiskUsage {
private final int maxCount;
public TotalCountLruDiskUsage(int maxCount) {
if (maxCount <= 0) {
throw new IllegalArgumentException("Max count must be positive number!");
}
this.maxCount = maxCount;
}
@Override
protected boolean accept(File file, long totalSize, int totalCount) {
return totalCount <= maxCount;
}
}
package com.danikula.videocache.file;
import java.io.File;
/**
* {@link DiskUsage} that uses LRU (Least Recently Used) strategy and trims cache size to max size if needed.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class TotalSizeLruDiskUsage extends LruDiskUsage {
private final long maxSize;
public TotalSizeLruDiskUsage(long maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("Max size must be positive number!");
}
this.maxSize = maxSize;
}
@Override
protected boolean accept(File file, long totalSize, int totalCount) {
return totalSize <= maxSize;
}
}
package com.danikula.videocache.file;
import java.io.File;
import java.io.IOException;
/**
* Unlimited version of {@link DiskUsage}.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class UnlimitedDiskUsage implements DiskUsage {
@Override
public void touch(File file) throws IOException {
// do nothing
}
}
package com.danikula.videocache.headers;
import java.util.HashMap;
import java.util.Map;
/**
* Empty {@link HeaderInjector} implementation.
*
* @author Lucas Nelaupe (https://github.com/lucas34).
*/
public class EmptyHeadersInjector implements HeaderInjector {
@Override
public Map<String, String> addHeaders(String url) {
return new HashMap<>();
}
}
package com.danikula.videocache.headers;
import java.util.Map;
/**
* Allows to add custom headers to server's requests.
*
* @author Lucas Nelaupe (https://github.com/lucas34).
*/
public interface HeaderInjector {
/**
* Adds headers to server's requests for corresponding url.
*
* @param url an url headers will be added for
* @return a map with headers, where keys are header's names, and values are header's values. {@code null} is not acceptable!
*/
Map<String, String> addHeaders(String url);
}
package com.danikula.videocache.sourcestorage;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import com.danikula.videocache.SourceInfo;
import static com.danikula.videocache.Preconditions.checkAllNotNull;
import static com.danikula.videocache.Preconditions.checkNotNull;
/**
* Database based {@link SourceInfoStorage}.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
class DatabaseSourceInfoStorage extends SQLiteOpenHelper implements SourceInfoStorage {
private static final String TABLE = "SourceInfo";
private static final String COLUMN_ID = "_id";
private static final String COLUMN_URL = "url";
private static final String COLUMN_LENGTH = "length";
private static final String COLUMN_MIME = "mime";
private static final String[] ALL_COLUMNS = new String[]{COLUMN_ID, COLUMN_URL, COLUMN_LENGTH, COLUMN_MIME};
private static final String CREATE_SQL =
"CREATE TABLE " + TABLE + " (" +
COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
COLUMN_URL + " TEXT NOT NULL," +
COLUMN_MIME + " TEXT," +
COLUMN_LENGTH + " INTEGER" +
");";
DatabaseSourceInfoStorage(Context context) {
super(context, "AndroidVideoCache.db", null, 1);
checkNotNull(context);
}
@Override
public void onCreate(SQLiteDatabase db) {
checkNotNull(db);
db.execSQL(CREATE_SQL);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
throw new IllegalStateException("Should not be called. There is no any migration");
}
@Override
public SourceInfo get(String url) {
checkNotNull(url);
Cursor cursor = null;
try {
cursor = getReadableDatabase().query(TABLE, ALL_COLUMNS, COLUMN_URL + "=?", new String[]{url}, null, null, null);
return cursor == null || !cursor.moveToFirst() ? null : convert(cursor);
} finally {
if (cursor != null) {
cursor.close();
}
}
}
@Override
public void put(String url, SourceInfo sourceInfo) {
checkAllNotNull(url, sourceInfo);
SourceInfo sourceInfoFromDb = get(url);
boolean exist = sourceInfoFromDb != null;
ContentValues contentValues = convert(sourceInfo);
if (exist) {
getWritableDatabase().update(TABLE, contentValues, COLUMN_URL + "=?", new String[]{url});
} else {
getWritableDatabase().insert(TABLE, null, contentValues);
}
}
@Override
public void release() {
close();
}
private SourceInfo convert(Cursor cursor) {
return new SourceInfo(
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_URL)),
cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_LENGTH)),
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MIME))
);
}
private ContentValues convert(SourceInfo sourceInfo) {
ContentValues values = new ContentValues();
values.put(COLUMN_URL, sourceInfo.url);
values.put(COLUMN_LENGTH, sourceInfo.length);
values.put(COLUMN_MIME, sourceInfo.mime);
return values;
}
}
package com.danikula.videocache.sourcestorage;
import com.danikula.videocache.SourceInfo;
/**
* {@link SourceInfoStorage} that does nothing.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class NoSourceInfoStorage implements SourceInfoStorage {
@Override
public SourceInfo get(String url) {
return null;
}
@Override
public void put(String url, SourceInfo sourceInfo) {
}
@Override
public void release() {
}
}
package com.danikula.videocache.sourcestorage;
import com.danikula.videocache.SourceInfo;
/**
* Storage for {@link SourceInfo}.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public interface SourceInfoStorage {
SourceInfo get(String url);
void put(String url, SourceInfo sourceInfo);
void release();
}
package com.danikula.videocache.sourcestorage;
import android.content.Context;
/**
* Simple factory for {@link SourceInfoStorage}.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class SourceInfoStorageFactory {
public static SourceInfoStorage newSourceInfoStorage(Context context) {
return new DatabaseSourceInfoStorage(context);
}
public static SourceInfoStorage newEmptySourceInfoStorage() {
return new NoSourceInfoStorage();
}
}
package com.danikula.videocache
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment