Commit 748a830d authored by mengcuiguang's avatar mengcuiguang

视频添加分布加载,添加视频预缓存

parent f8c24ce4
......@@ -14,6 +14,7 @@
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/oaid" />
<option value="$PROJECT_DIR$/rxpay" />
<option value="$PROJECT_DIR$/videocache" />
<option value="$PROJECT_DIR$/wxpay" />
</set>
</option>
......
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_15" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
......
......@@ -221,6 +221,9 @@ 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'
implementation 'com.tencent.bugly:nativecrashreport:3.0'
......
......@@ -20,6 +20,7 @@ import com.mints.wisdomclean.mvp.presenters.HomePresenter
import com.mints.wisdomclean.mvp.views.HomeView
import com.mints.wisdomclean.ui.adapter.HomeVideoPageAdapter
import com.mints.wisdomclean.ui.fragment.base.BaseFragment
import com.mints.wisdomclean.video.VideoActivity
import com.scwang.smartrefresh.layout.api.RefreshLayout
import com.scwang.smartrefresh.layout.listener.OnRefreshListener
import com.youth.banner.Banner
......@@ -57,6 +58,7 @@ class MainFragment : BaseFragment(), HomeView, View.OnClickListener, OnRefreshLi
homePresenter.attachView(this)
initView()
initListener()
initVp()
......@@ -126,16 +128,16 @@ class MainFragment : BaseFragment(), HomeView, View.OnClickListener, OnRefreshLi
}
private fun initListener() {
// iv_vip.setOnClickListener(this)
tv_test.setOnClickListener(this)
}
override fun onClick(v: View?) {
if (AntiShake.check(v?.id)) return
when (v?.id) {
// R.id.iv_vip -> {
// readyGo(VipActivity::class.java)
// }
R.id.tv_test -> {
readyGo(VideoActivity::class.java)
}
}
}
......@@ -214,4 +216,6 @@ class MainFragment : BaseFragment(), HomeView, View.OnClickListener, OnRefreshLi
}
}
\ No newline at end of file
package com.mints.wisdomclean.video;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import com.mints.wisdomclean.R;
import cn.jzvd.JzvdStd;
public class JzvdStdTikTok extends JzvdStd {
public JzvdStdTikTok(Context context) {
super(context);
}
public JzvdStdTikTok(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void init(Context context) {
super.init(context);
bottomContainer.setVisibility(GONE);
topContainer.setVisibility(GONE);
bottomProgressBar.setVisibility(GONE);
posterImageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
}
//changeUiTo 真能能修改ui的方法
@Override
public void changeUiToNormal() {
super.changeUiToNormal();
bottomContainer.setVisibility(GONE);
topContainer.setVisibility(GONE);
}
@Override
public void setAllControlsVisiblity(int topCon, int bottomCon, int startBtn, int loadingPro,
int posterImg, int bottomPro, int retryLayout) {
topContainer.setVisibility(INVISIBLE);
bottomContainer.setVisibility(INVISIBLE);
startButton.setVisibility(startBtn);
loadingProgressBar.setVisibility(loadingPro);
posterImageView.setVisibility(posterImg);
bottomProgressBar.setVisibility(GONE);
mRetryLayout.setVisibility(retryLayout);
}
@Override
public void dissmissControlView() {
if (state != STATE_NORMAL
&& state != STATE_ERROR
&& state != STATE_AUTO_COMPLETE) {
post(() -> {
bottomContainer.setVisibility(View.INVISIBLE);
topContainer.setVisibility(View.INVISIBLE);
startButton.setVisibility(View.INVISIBLE);
if (clarityPopWindow != null) {
clarityPopWindow.dismiss();
}
if (screen != SCREEN_TINY) {
bottomProgressBar.setVisibility(View.GONE);
}
});
}
}
@Override
public void onClickUiToggle() {
super.onClickUiToggle();
Log.i(TAG, "click blank");
startButton.performClick();
bottomContainer.setVisibility(GONE);
topContainer.setVisibility(GONE);
}
public void updateStartImage() {
if (state == STATE_PLAYING) {
startButton.setVisibility(VISIBLE);
startButton.setImageResource(R.drawable.tiktok_play_tiktok);
replayTextView.setVisibility(GONE);
} else if (state == STATE_ERROR) {
startButton.setVisibility(INVISIBLE);
replayTextView.setVisibility(GONE);
} else if (state == STATE_AUTO_COMPLETE) {
startButton.setVisibility(VISIBLE);
startButton.setImageResource(R.drawable.tiktok_play_tiktok);
replayTextView.setVisibility(VISIBLE);
} else {
startButton.setImageResource(R.drawable.tiktok_play_tiktok);
replayTextView.setVisibility(GONE);
}
}
@Override
public void onCompletion() {
super.onCompletion();
this.onVideoCompletion.onVideoCompletion();
}
public void setOnVideoCompletion(OnVideoCompletion onVideoCompletion) {
this.onVideoCompletion = onVideoCompletion;
}
private OnVideoCompletion onVideoCompletion;
public interface OnVideoCompletion {
void onVideoCompletion();
}
}
package com.cwj.updownshortvideo
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.widget.ImageView
import android.widget.Toast
import cn.jzvd.JZUtils
import cn.jzvd.Jzvd
import cn.jzvd.JzvdStd
import com.mints.wisdomclean.R
/**
* 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.GONE
currentTimeTextView.visibility = View.GONE
totalTimeTextView.visibility = View.GONE //当前时间
fullscreenButton.visibility = View.GONE //放大按钮
topContainer.visibility = View.GONE
progressBar.visibility = View.GONE //控制的
loadingProgressBar.visibility = View.GONE //加载loaing
bottomProgressBar.visibility = View.VISIBLE //最底部的进度
posterImageView.scaleType = ImageView.ScaleType.FIT_CENTER
}
override fun setUp(
url: String?,
title: String?,
screen: Int,
mediaInterfaceClass: Class<*>?
) {
super.setUp(url, title, screen, mediaInterfaceClass)
}
//changeUiTo 真能能修改ui的方法
override fun changeUiToNormal() {
super.changeUiToNormal()
bottomContainer.visibility = View.GONE
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.VISIBLE
mRetryLayout.visibility = retryLayout
}
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.GONE
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")
}
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))
}
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
}
}
}
package com.cwj.updownshortvideo
/**
* author : ChenWenJie
* email : 1181620038@qq.com
* date : 2020/9/22
* desc : 监听接口
*/
interface OnRecyViewListener {
/*初始化完成*/
fun onInitComplete()
/*释放的监听*/
fun onPageRelease(isNext: Boolean, position: Int)
/*选中的监听以及判断是否滑动到底部*/
fun onPageSelected(position: Int, isBottom: Boolean)
}
\ No newline at end of file
package com.mints.wisdomclean.video;
/**
* Created by 钉某人
* github: https://github.com/DingMouRen
* email: naildingmouren@gmail.com
* 用于ViewPagerLayoutManager的监听
*/
public interface OnViewPagerListener {
/*初始化完成*/
void onInitComplete();
/*释放的监听*/
void onPageRelease(boolean isNext, int position);
/*选中的监听以及判断是否滑动到底部*/
void onPageSelected(int position, boolean isBottom);
}
package com.cwj.updownshortvideo
import android.content.Context
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
/**
* author : ChenWenJie
* email : 1181620038@qq.com
* date : 2020/9/22
* desc : 定义管理器,一item一屏 监听滑动状态
*/
class RecyViewLayoutManager : LinearLayoutManager {
private var mPagerSnapHelper: PagerSnapHelper? = null
private var mOnRecycleViewListener: OnRecyViewListener? = null
private var mRecyclerView: RecyclerView? = null
private var mDrift //位移,用来判断移动方向
= 0
private val mChildAttachStateChangeListener: RecyclerView.OnChildAttachStateChangeListener =
object : RecyclerView.OnChildAttachStateChangeListener {
override fun onChildViewDetachedFromWindow(view: View) {
if (mDrift >= 0) {
if (mOnRecycleViewListener != null) {
mOnRecycleViewListener!!.onPageRelease(true, getPosition(view))
}
} else {
if (mOnRecycleViewListener != null) {
mOnRecycleViewListener!!.onPageRelease(false, getPosition(view))
}
}
}
override fun onChildViewAttachedToWindow(view: View) {
if (mOnRecycleViewListener != null && getChildCount() === 1) {
mOnRecycleViewListener!!.onInitComplete()
}
}
}
constructor(context: Context?, orientation: Int) : super(
context,
orientation,
false
) {
init()
}
constructor(
context: Context?,
orientation: Int,
reverseLayout: Boolean
) : super(context, orientation, reverseLayout) {
init()
}
private fun init() {
mPagerSnapHelper = PagerSnapHelper()
}
override fun onAttachedToWindow(view: RecyclerView?) {
super.onAttachedToWindow(view)
mPagerSnapHelper?.attachToRecyclerView(view)
mRecyclerView = view
mRecyclerView?.addOnChildAttachStateChangeListener(mChildAttachStateChangeListener)
}
override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {
super.onLayoutChildren(recycler, state)
//
}
/**
* 滑动状态的改变
* 缓慢拖拽-> SCROLL_STATE_DRAGGING
* 快速滚动-> SCROLL_STATE_SETTLING
* 空闲状态-> SCROLL_STATE_IDLE
*
* @param state
*/
override fun onScrollStateChanged(state: Int) {
when (state) {
RecyclerView.SCROLL_STATE_IDLE -> {
val viewIdle: View = mPagerSnapHelper!!.findSnapView(this)!!
val positionIdle: Int = getPosition(viewIdle)
if (mOnRecycleViewListener != null && getChildCount() === 1) {
mOnRecycleViewListener!!.onPageSelected(
positionIdle,
positionIdle == getItemCount() - 1
)
}
}
RecyclerView.SCROLL_STATE_DRAGGING -> {
val viewDrag: View? = mPagerSnapHelper!!.findSnapView(this)!!
val positionDrag: Int = getPosition(viewDrag!!)
}
RecyclerView.SCROLL_STATE_SETTLING -> {
val viewSettling: View = mPagerSnapHelper!!.findSnapView(this)!!
val positionSettling: Int = getPosition(viewSettling)
}
}
}
/**
* 监听竖直方向的相对偏移量
*
* @param dy
* @param recycler
* @param state
* @return
*/
override fun scrollVerticallyBy(
dy: Int,
recycler: RecyclerView.Recycler?,
state: RecyclerView.State?
): Int {
mDrift = dy
return super.scrollVerticallyBy(dy, recycler, state)
}
/**
* 监听水平方向的相对偏移量
*
* @param dx
* @param recycler
* @param state
* @return
*/
override fun scrollHorizontallyBy(
dx: Int,
recycler: RecyclerView.Recycler?,
state: RecyclerView.State?
): Int {
mDrift = dx
return super.scrollHorizontallyBy(dx, recycler, state)
}
/**
* 设置监听
*
* @param listener
*/
fun setOnViewPagerListener(listener: OnRecyViewListener?) {
mOnRecycleViewListener = listener
}
companion object {
private const val TAG = "ViewPagerLayoutManager"
}
}
package com.mints.wisdomclean.video;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.mints.wisdomclean.R;
import cn.jzvd.JZDataSource;
import cn.jzvd.Jzvd;
public class TikTokRecyclerViewAdapter extends RecyclerView.Adapter<TikTokRecyclerViewAdapter.MyViewHolder> {
public static final String TAG = "AdapterTikTokRecyclerView";
int[] videoIndexs = {0, 1, 2, 3, 4, 5};
private Context context;
public TikTokRecyclerViewAdapter(Context context) {
this.context = context;
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
MyViewHolder holder = new MyViewHolder(LayoutInflater.from(
context).inflate(R.layout.item_tiktok, parent,
false));
return holder;
}
@SuppressLint("LongLogTag")
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
position = position % videoIndexs.length;
Log.i(TAG, "onBindViewHolder [" + holder.jzvdStd.hashCode() + "] position=" + position);
// JZDataSource jzDataSource = new JZDataSource(
// UrlsKt.getVl3()[position],
// UrlsKt.getTl3()[position]);
JZDataSource jzDataSource;
if (position % 2 == 0) {
jzDataSource = new JZDataSource(
"http://oss-cn-beijing.aliyuncs.com/static-thefair-bj/eyepetizer/pgc_video/video_summary/219338.mp4",
"测试测试测试1");
} else {
jzDataSource = new JZDataSource(
"http://oss-cn-beijing.aliyuncs.com/static-thefair-bj/eyepetizer/pgc_video/video_summary/220775.mp4",
"测试测试测试2");
}
holder.jzvdStd.setOnVideoCompletion(onVideoCompletion);
// jzDataSource.looping = true;
jzDataSource.looping = false;
holder.jzvdStd.setUp(jzDataSource, Jzvd.SCREEN_NORMAL);
Glide.with(holder.jzvdStd.getContext()).load(UrlsKt.getPl3()[position]).into(holder.jzvdStd.posterImageView);
}
public void setOnVideoCompletion(JzvdStdTikTok.OnVideoCompletion onVideoCompletion) {
this.onVideoCompletion = onVideoCompletion;
}
private JzvdStdTikTok.OnVideoCompletion onVideoCompletion;
@Override
public int getItemCount() {
// return videoIndexs.length;
return Integer.MAX_VALUE;
}
static class MyViewHolder extends RecyclerView.ViewHolder {
JzvdStdTikTok jzvdStd;
public MyViewHolder(View itemView) {
super(itemView);
jzvdStd = itemView.findViewById(R.id.videoplayer);
}
}
}
package com.cwj.updownshortvideo
import android.app.Activity
import android.net.Uri
import android.util.Log
import android.widget.ImageView
import cn.jzvd.JZUtils
import cn.jzvd.Jzvd
import cn.jzvd.JzvdStd
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import com.mints.wisdomclean.MintsApplication
import com.mints.wisdomclean.R
/**
* author : ChenWenJie
* email : 1181620038@qq.com
* date : 2020/9/22
* desc : 适配器
*/
class VideoAdapter(var activity: Activity) :
BaseQuickAdapter<VideoBean, BaseViewHolder>(R.layout.item_video) {
override fun convert(holder: BaseViewHolder, item: VideoBean) {
//圆形用户头像
val requestOptions = RequestOptions.circleCropTransform()
Glide.with(activity).load(item.user_image).apply(requestOptions)
.into(holder.getView(R.id.user_iv))
//用户名
holder.setText(R.id.username_tv, item.user_name)
//标题
holder.setText(R.id.usertitle_tv, item.video_title)
//缩略图
Glide.with(activity).load(item.video_image)
.into(holder?.getView<JzvdStdTikTok>(R.id.jz_video)!!.posterImageView)
//声明 代理服务缓存
val proxy = MintsApplication.StaticParams.getProxy()
//这个缓存下一个
if (holder?.layoutPosition!! + 1 < itemCount) {
var item1 = getItem(holder?.layoutPosition!! + 1)
//缓存下一个 10秒
proxy!!.preLoad(item1!!.video_path, 10)
}
//缓存当前,播放当前
var proxyUrl =proxy?.getProxyUrl(item.video_path).toString() //设置视
setPlay(holder.getView(R.id.jz_video),proxyUrl)
}
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_FULLSCREEN)
}
}
\ No newline at end of file
package com.cwj.updownshortvideo
/**
* author : ChenWenJie
* email : 1181620038@qq.com
* date : 2020/9/22
* desc : 视频实体类
*/
class VideoBean {
// ID
var id: Int = 0
//用户名
var user_name: String = ""
//用户头像
var user_image: String = ""
//视频标题
var video_title: String = ""
//视频路径
var video_path: String = ""
//视频图片
var video_image: String = ""
constructor(
id: Int,
user_name: String,
user_image: String,
video_title: String,
video_path: String,
video_image: String
) {
this.id = id
this.user_name = user_name
this.user_image = user_image
this.video_title = video_title
this.video_path = video_path
this.video_image = video_image
}
}
\ No newline at end of file
package com.mints.wisdomclean.video;
import android.content.Context;
import android.view.View;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.PagerSnapHelper;
import androidx.recyclerview.widget.RecyclerView;
/**
* Created by 钉某人
* github: https://github.com/DingMouRen
* email: naildingmouren@gmail.com
*/
public class ViewPagerLayoutManager extends LinearLayoutManager {
private static final String TAG = "ViewPagerLayoutManager";
private PagerSnapHelper mPagerSnapHelper;
private OnViewPagerListener mOnViewPagerListener;
private RecyclerView mRecyclerView;
private int mDrift;//位移,用来判断移动方向
private RecyclerView.OnChildAttachStateChangeListener mChildAttachStateChangeListener = new RecyclerView.OnChildAttachStateChangeListener() {
@Override
public void onChildViewAttachedToWindow(View view) {
if (mOnViewPagerListener != null && getChildCount() == 1) {
mOnViewPagerListener.onInitComplete();
}
}
@Override
public void onChildViewDetachedFromWindow(View view) {
if (mDrift >= 0) {
if (mOnViewPagerListener != null)
mOnViewPagerListener.onPageRelease(true, getPosition(view));
} else {
if (mOnViewPagerListener != null)
mOnViewPagerListener.onPageRelease(false, getPosition(view));
}
}
};
public ViewPagerLayoutManager(Context context, int orientation) {
super(context, orientation, false);
init();
}
public ViewPagerLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
init();
}
private void init() {
mPagerSnapHelper = new PagerSnapHelper();
}
@Override
public void onAttachedToWindow(RecyclerView view) {
super.onAttachedToWindow(view);
mPagerSnapHelper.attachToRecyclerView(view);
this.mRecyclerView = view;
mRecyclerView.addOnChildAttachStateChangeListener(mChildAttachStateChangeListener);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
super.onLayoutChildren(recycler, state);
}
/**
* 滑动状态的改变
* 缓慢拖拽-> SCROLL_STATE_DRAGGING
* 快速滚动-> SCROLL_STATE_SETTLING
* 空闲状态-> SCROLL_STATE_IDLE
*
* @param state
*/
@Override
public void onScrollStateChanged(int state) {
switch (state) {
case RecyclerView.SCROLL_STATE_IDLE:
View viewIdle = mPagerSnapHelper.findSnapView(this);
if (viewIdle != null) {
int positionIdle = getPosition(viewIdle);
if (mOnViewPagerListener != null && getChildCount() == 1) {
mOnViewPagerListener.onPageSelected(positionIdle, positionIdle == getItemCount() - 1);
}
}
break;
}
}
/**
* 监听竖直方向的相对偏移量
*
* @param dy
* @param recycler
* @param state
* @return
*/
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
this.mDrift = dy;
return super.scrollVerticallyBy(dy, recycler, state);
}
/**
* 监听水平方向的相对偏移量
*
* @param dx
* @param recycler
* @param state
* @return
*/
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
this.mDrift = dx;
return super.scrollHorizontallyBy(dx, recycler, state);
}
/**
* 设置监听
*
* @param listener
*/
public void setOnViewPagerListener(OnViewPagerListener listener) {
this.mOnViewPagerListener = listener;
}
}
package com.mints.wisdomclean.video
// Chrome浏览器和饺子demo(android.MediaPlayer)相同时间相同网络环境播放,速度相差很远,为什么。
// ijk好像略快,但没有电脑的chrome快。红米手机的自带浏览器可以完整观看视频无需中途加载。
val cn = "http://8.136.101.204"
val us = "http://videos.jzvd.org"
var server_name: String = cn
val ldjVideos = arrayOf(
"$server_name/v/ldj/01-ldj.mp4",
"$server_name/v/ldj/02-ldj.mp4",
"$server_name/v/ldj/03-ldj.mp4",
"$server_name/v/ldj/04-ldj.mp4",
"$server_name/v/ldj/05-ldj.mp4",
"$server_name/v/ldj/06-ldj.mp4",
"$server_name/v/ldj/07-ldj.mp4",
"$server_name/v/ldj/08-ldj.mp4",
"$server_name/v/ldj/09-ldj.mp4",
"$server_name/v/ldj/10-ldj.mp4",
"$server_name/v/ldj/11-ldj.mp4",
"$server_name/v/ldj/12-ldj.mp4",
"$server_name/v/ldj/13-ldj.mp4",
"$server_name/v/ldj/14-ldj.mp4",
"$server_name/v/ldj/15-ldj.mp4",
"$server_name/v/ldj/16-ldj.mp4",
"$server_name/v/ldj/17-ldj.mp4",
"$server_name/v/ldj/18-ldj.mp4",
"$server_name/v/ldj/19-ldj.mp4",
"$server_name/v/ldj/20-ldj.mp4",
"$server_name/v/ldj/21-ldj.mp4",
"$server_name/v/ldj/22-ldj.mp4",
"$server_name/v/ldj/23-ldj.mp4",
"$server_name/v/ldj/24-ldj.mp4",
"$server_name/v/ldj/25-ldj.mp4",
"$server_name/v/ldj/26-ldj.mp4",
"$server_name/v/ldj/27-ldj.mp4",
"$server_name/v/ldj/28-ldj.mp4",
"$server_name/v/ldj/29-ldj.mp4",
"$server_name/v/ldj/30-ldj.mp4",
"$server_name/v/ldj/31-ldj.mp4",
"$server_name/v/ldj/32-ldj.mp4",
"$server_name/v/ldj/33-ldj.mp4",
"$server_name/v/ldj/34-ldj.mp4",
"$server_name/v/ldj/35-ldj.mp4",
"$server_name/v/ldj/36-ldj.mp4",
"$server_name/v/ldj/37-ldj.mp4",
"$server_name/v/ldj/38-ldj.mp4",
"$server_name/v/ldj/39-ldj.mp4",
"$server_name/v/ldj/40-ldj.mp4",
"$server_name/v/ldj/41-ldj.mp4",
"$server_name/v/ldj/42-ldj.mp4",
"$server_name/v/ldj/43-ldj.mp4",
"$server_name/v/ldj/44-ldj.mp4",
"$server_name/v/ldj/45-ldj.mp4"
)
val cndVideos = arrayOf(
"https://jzvd.nathen.cn/video/cfe6c30-1767b1bc21f-0007-1823-c86-de200.mp4",//三个不同分辨率
"https://jzvd.nathen.cn/0339d49439f947419576c33a0aa51545/79e1a938b0d2435d85bd964a77640506-f4e986e3e38ed3f473f7ba82bc07e188-ld.mp4",
"https://jzvd.nathen.cn/0339d49439f947419576c33a0aa51545/79e1a938b0d2435d85bd964a77640506-8924c7da92ebd789d315bc5de0a81059-fd.mp4",
"https://jzvd.nathen.cn/video/59aa468b-1767b6d891e-0007-1823-c86-de200.mp4",//饺子还小
"https://jzvd.nathen.cn/video/25ae1b1c-1767b2a5e44-0007-1823-c86-de200.mp4",//饺子还年轻
"https://jzvd.nathen.cn/video/5a6465ff-1767b2a5e28-0007-1823-c86-de200.mp4"
)
val cndThumbnail = arrayOf(
"https://jzvd.nathen.cn/snapshot/0339d49439f947419576c33a0aa5154500005.jpg",
"",
"",
"https://jzvd.nathen.cn/snapshot/61c99f9225c04b24a1d0374e9a3f006700004.jpg",
"https://jzvd.nathen.cn/snapshot/044ef6cf452d48b795cea0a96ee4ea4100002.jpg",
"https://jzvd.nathen.cn/snapshot/a172cc6442ff40ffb826829fc78f83b700005.jpg"
)
val videos = arrayOf(
//width > height
"$server_name/v/饺子主动.mp4",
"$server_name/v/饺子运动.mp4",
"$server_name/v/饺子有活.mp4",
"$server_name/v/饺子星光.mp4",
"$server_name/v/饺子想吹.mp4",
"$server_name/v/饺子汪汪.mp4",
"$server_name/v/饺子偷人.mp4",
"$server_name/v/饺子跳.mp4",
"$server_name/v/饺子受不了.mp4",
"$server_name/v/饺子三位.mp4",
"$server_name/v/饺子起飞.mp4",
"$server_name/v/饺子你听.mp4",
"$server_name/v/饺子可以了.mp4",
"$server_name/v/饺子还小.mp4",
"$server_name/v/饺子高兴.mp4",
"$server_name/v/饺子高冷.mp4",
"$server_name/v/饺子堵住了.mp4",
"$server_name/v/饺子都懂.mp4",
"$server_name/v/饺子打电话.mp4",
"$server_name/v/饺子不服.mp4",
//height > width
"$server_name/v/饺子还年轻.mp4",
"$server_name/v/饺子好妈妈.mp4",
"$server_name/v/饺子可以.mp4",
"$server_name/v/饺子挺住.mp4",
"$server_name/v/饺子想听.mp4",
"$server_name/v/饺子真会.mp4",
"$server_name/v/饺子真萌.mp4"
)
val thumbnails = arrayOf(
"$server_name/v/饺子主动.jpg",
"$server_name/v/饺子运动.jpg",
"$server_name/v/饺子有活.jpg",
"$server_name/v/饺子星光.jpg",
"$server_name/v/饺子想吹.jpg",
"$server_name/v/饺子汪汪.jpg",
"$server_name/v/饺子偷人.jpg",
"$server_name/v/饺子跳.jpg",
"$server_name/v/饺子受不了.jpg",
"$server_name/v/饺子三位.jpg",
"$server_name/v/饺子起飞.jpg",
"$server_name/v/饺子你听.jpg",
"$server_name/v/饺子可以了.jpg",
"$server_name/v/饺子还小.jpg",
"$server_name/v/饺子高兴.jpg",
"$server_name/v/饺子高冷.jpg",
"$server_name/v/饺子堵住了.jpg",
"$server_name/v/饺子都懂.jpg",
"$server_name/v/饺子打电话.jpg",
"$server_name/v/饺子不服.jpg",
//height > width
"$server_name/v/饺子还年轻.jpg",
"$server_name/v/饺子好妈妈.jpg",
"$server_name/v/饺子可以.jpg",
"$server_name/v/饺子挺住.jpg",
"$server_name/v/饺子想听.jpg",
"$server_name/v/饺子真会.jpg",
"$server_name/v/饺子真萌.jpg"
)
val titles = arrayOf(
//width > height
"饺子主动",
"饺子运动",
"饺子有活",
"饺子星光",
"饺子想吹",
"饺子汪汪",
"饺子偷人",
"饺子跳",
"饺子受不了",
"饺子三位",
"饺子起飞",
"饺子你听",
"饺子可以了",
"饺子还小",
"饺子高兴",
"饺子高冷",
"饺子堵住了",
"饺子都懂",
"饺子打电话",
"饺子不服",
//height > width
"饺子还年轻",
"饺子好妈妈",
"饺子可以",
"饺子挺住",
"饺子想听",
"饺子真会",
"饺子真萌"
)
val vl1 = videos.copyOfRange(0, 9)
val pl1 = thumbnails.copyOfRange(0, 9)
val tl1 = titles.copyOfRange(0, 9)
val vl2 = videos.copyOfRange(10, 20)
val pl2 = thumbnails.copyOfRange(10, 20)
val tl2 = titles.copyOfRange(10, 20)
val vl3 = videos.copyOfRange(20, 26)
val pl3 = thumbnails.copyOfRange(20, 26)
val tl3 = titles.copyOfRange(20, 26)
val vll = arrayOf(
vl1, vl2, vl3
)
val pll = arrayOf(
pl1, pl2, pl3
)
val tll = arrayOf(
tl1, tl2, tl3
)
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="1117"
android:viewportHeight="1024">
<path
android:pathData="M802.85,0C706.75,0 617.38,47.54 558.55,125.92 499.74,47.51 410.41,0 314.24,0 140.97,0 0,151.96 0,338.73c0,111.43 50.49,189.6 91.07,252.4 117.95,182.46 414.53,409.48 427.1,419.03 12.1,9.22 26.25,13.84 40.34,13.84 14.15,0 28.3,-4.62 40.37,-13.84 12.6,-9.56 309.19,-236.61 427.1,-419.03C1066.57,528.32 1117.09,450.16 1117.09,338.73 1117.09,151.92 976.12,0 802.85,0z"
android:fillColor="#fff"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FFF"
android:pathData="M512.05,50.98C229.2,50.98 0,238.98 0,470.93c0,212.42 192.48,387.52 442.02,415.65 -10.72,15.2 -24.93,34.37 -35.73,45.18a24.11,24.11 0,0 0,-7.58 4.83,20.56 20.56,0 0,0 -6.11,14.21c-0.1,3.62 0.99,6.91 2.5,9.92 0.61,10 9.5,17.81 44.93,7.3 59.62,-17.7 95.76,-25.44 182.22,-56.02 60.32,-21.33 114.13,-53.34 155.42,-82.56C925.17,755.7 1024,622.96 1024,470.93 1024,238.98 794.8,50.98 512.05,50.98zM256.02,533.98c-35.33,0 -64.02,-28.24 -64.02,-63.06 0,-34.75 28.7,-62.98 64.02,-62.98 35.31,0 64.03,28.22 64.03,62.98 0.02,34.82 -28.72,63.06 -64.03,63.06zM512.05,533.98c-35.42,0 -64.03,-28.24 -64.03,-63.06 0,-34.75 28.61,-62.98 64.03,-62.98 35.31,0 63.9,28.22 63.9,62.98 0,34.82 -28.59,63.06 -63.9,63.06zM767.98,533.98c-35.33,0 -63.92,-28.24 -63.92,-63.06 0,-34.75 28.59,-62.98 63.92,-62.98 35.41,0 64.02,28.22 64.02,62.98 0,34.82 -28.61,63.06 -64.02,63.06z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#fff"
android:pathData="M50.56,993.33l-44.69,3.96 89.59,-3.96c0,-293.18 208.49,-410.87 404.05,-426.17v248.83l522.06,-406.28L499.53,3.64v217.13c-135.78,8.92 -246.56,59.46 -329.68,150.44C-25.3,584.82 2.05,955.19 5.83,996.76l44.74,-3.44z"/>
</vector>
......@@ -114,7 +114,7 @@
android:layout_marginLeft="30dp"
android:layout_marginRight="30dp"
android:gravity="center"
android:text="Copyright © 2020 Inc.."
android:text="Copyright © 2020 Inc."
android:textColor="#454A69"
android:textSize="12sp" />
</LinearLayout>
......
......@@ -6,7 +6,7 @@
tools:context=".video.VideoActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_video"
android:id="@+id/recy"
android:layout_width="match_parent"
android:layout_height="match_parent" />
......
......@@ -16,6 +16,7 @@
android:layout_marginTop="20dp">
<TextView
android:id="@+id/tv_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
......
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#77090909">
<com.cwj.updownshortvideo.JzvdStdTikTok
android:id="@+id/jz_video"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/ll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@id/usertitle_tv"
app:layout_constraintLeft_toLeftOf="parent">
<ImageView
android:id="@+id/user_iv"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="2dp"
android:scaleType="fitXY" />
<TextView
android:id="@+id/username_tv"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:ellipsize="end"
android:gravity="center"
android:maxEms="9"
android:maxLines="1"
android:text="飞翔的企鹅"
android:textColor="#fff"
android:textSize="16sp" />
</LinearLayout>
<TextView
android:id="@+id/usertitle_tv"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_marginLeft="10dp"
android:ellipsize="end"
android:gravity="center|left"
android:maxEms="14"
android:maxLines="3"
android:text="飞翔的企鹅飞翔的企鹅飞翔的企鹅飞翔的企鹅飞翔/n的企鹅飞翔的企鹅"
android:textColor="#fff"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
app:layout_constraintBottom_toTopOf="@id/usertitle_tv"
android:orientation="vertical"
app:layout_constraintRight_toRightOf="parent">
<ImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginTop="10dp"
android:scaleType="fitXY"
android:id="@+id/zan_iv"
android:src="@drawable/ic_aixin" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="199.9w"
android:id="@+id/zan_num_tv"
android:textColor="#fff"
android:textSize="14sp" />
<ImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginTop="10dp"
android:scaleType="fitXY"
android:src="@drawable/ic_pl" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="12312"
android:id="@+id/zan_price_tv"
android:textColor="#fff"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="#fff"
android:textSize="14sp" />
<ImageView
android:id="@+id/share_iv"
android:layout_width="35dp"
android:layout_height="35dp"
android:scaleType="fitXY"
android:src="@drawable/ic_zhunfa" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="#fff"
android:text="345"
android:textSize="14sp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
......@@ -2,4 +2,5 @@ include ':app'
include ':oaid'
include ':rxpay'
include ':alipay'
include ':wxpay'
\ No newline at end of file
include ':wxpay'
include ':videocache'
\ No newline at end of file
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