diff --git a/app/src/main/java/com/gh/download/cache/CacheInfo.java b/app/src/main/java/com/gh/download/cache/CacheInfo.java new file mode 100644 index 0000000000..31d07ab97c --- /dev/null +++ b/app/src/main/java/com/gh/download/cache/CacheInfo.java @@ -0,0 +1,41 @@ +package com.gh.download.cache; + +public class CacheInfo { + public static final long TOTAL_ERROR = -1;//获取进度失败 + private String url; + private long total; + private long progress; + private String fileName; + + public CacheInfo(String url) { + this.url = url; + } + + public String getUrl() { + return url; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public long getTotal() { + return total; + } + + public void setTotal(long total) { + this.total = total; + } + + public long getProgress() { + return progress; + } + + public void setProgress(long progress) { + this.progress = progress; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/download/cache/CacheManager.java b/app/src/main/java/com/gh/download/cache/CacheManager.java new file mode 100644 index 0000000000..66dcc71fde --- /dev/null +++ b/app/src/main/java/com/gh/download/cache/CacheManager.java @@ -0,0 +1,206 @@ +package com.gh.download.cache; + +import com.danikula.videocache.file.FileNameGenerator; +import com.danikula.videocache.file.Md5FileNameGenerator; +import com.halo.assistant.HaloApp; +import com.shuyu.gsyvideoplayer.utils.StorageUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.Observable; +import io.reactivex.ObservableEmitter; +import io.reactivex.ObservableOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import okhttp3.Call; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class CacheManager { + private static final AtomicReference INSTANCE = new AtomicReference<>(); + private HashMap downCalls; + private OkHttpClient mClient; + private File cacheDirectory = StorageUtils.getIndividualCacheDirectory(HaloApp.getInstance().getApplication()); + private FileNameGenerator generator = new Md5FileNameGenerator(); + private final String TEMP_POSTFIX = ".download"; + private final int preLength = 5 * 1024 * 1024;//预加载大小 + + public static CacheManager getInstance() { + for (; ; ) { + CacheManager current = INSTANCE.get(); + if (current != null) { + return current; + } + current = new CacheManager(); + if (INSTANCE.compareAndSet(null, current)) { + return current; + } + } + } + + private CacheManager() { + downCalls = new HashMap<>(); + mClient = new OkHttpClient.Builder().build(); + } + + public void download(String url, CacheObserver cacheObserver) { + //当前url已下载完成则不再下载 + for (File file : getAllFile()) { + if (file.getName().equals(generator.generate(url))) { + return; + } + } + Observable.just(url) + .filter(s -> !downCalls.containsKey(s))//call的map已经有了,就证明正在下载,则这次不下载 + .flatMap(s -> Observable.just(createDownInfo(s))) + .map(this::getRealFileName) + .flatMap(cacheInfo -> Observable.create(new DownloadSubscribe(cacheInfo))) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe(cacheObserver); + + } + + public void cancel(String url) { + Call call = downCalls.get(url); + if (call != null) { + call.cancel(); + } + downCalls.remove(url); + } + + /** + * 创建DownInfo + * + * @param url 请求网址 + * @return DownInfo + */ + private CacheInfo createDownInfo(String url) { + CacheInfo cacheInfo = new CacheInfo(url); + long contentLength = getContentLength(url);//获得文件大小 + cacheInfo.setTotal(contentLength); + String fileName = generator.generate(url) + TEMP_POSTFIX; + cacheInfo.setFileName(fileName); + return cacheInfo; + } + + private CacheInfo getRealFileName(CacheInfo cacheInfo) { + String fileName = cacheInfo.getFileName(); + long downloadLength = 0; + if (!cacheDirectory.exists()) { + cacheDirectory.mkdir(); + } + File file = new File(cacheDirectory, fileName); + if (file.exists()) { + //找到了文件,代表已经下载过,则获取其长度 + downloadLength = file.length(); + } + //设置改变过的文件名/大小 + cacheInfo.setProgress(downloadLength); + cacheInfo.setFileName(file.getName()); + return cacheInfo; + } + + + private class DownloadSubscribe implements ObservableOnSubscribe { + private CacheInfo cacheInfo; + + public DownloadSubscribe(CacheInfo cacheInfo) { + this.cacheInfo = cacheInfo; + } + + @Override + public void subscribe(ObservableEmitter e) throws Exception { + String url = cacheInfo.getUrl(); + long downloadLength = cacheInfo.getProgress();//已经下载好的长度 + long contentLength = cacheInfo.getTotal();//文件的总长度 + if (downloadLength >= preLength) { + e.onComplete(); + return; + } + e.onNext(cacheInfo); + Request request = new Request.Builder() + .addHeader("RANGE", "bytes=" + downloadLength + "-" + (contentLength > preLength ? preLength : contentLength)) + .url(url) + .build(); + Call call = mClient.newCall(request); + downCalls.put(url, call); + Response response = call.execute(); + File file = new File(cacheDirectory, cacheInfo.getFileName()); + InputStream is = null; + FileOutputStream fileOutputStream = null; + try { + is = response.body().byteStream(); + fileOutputStream = new FileOutputStream(file, true); + byte[] buffer = new byte[2048]; + int len; + while ((len = is.read(buffer)) != -1) { + fileOutputStream.write(buffer, 0, len); + downloadLength += len; + cacheInfo.setProgress(downloadLength); + e.onNext(cacheInfo); + } + fileOutputStream.flush(); + downCalls.remove(url); + } finally { + if (is != null) { + is.close(); + } + if (fileOutputStream != null) { + fileOutputStream.close(); + } + } + if (file.length() == contentLength){ + file.renameTo(new File(file.getPath().substring(0,file.getPath().lastIndexOf(TEMP_POSTFIX)))); + } + e.onComplete(); + } + } + + /** + * 获取下载长度 + * + * @param downloadUrl + * @return + */ + private long getContentLength(String downloadUrl) { + Request request = new Request.Builder() + .url(downloadUrl) + .build(); + try { + Response response = mClient.newCall(request).execute(); + if (response != null && response.isSuccessful()) { + long contentLength = response.body().contentLength(); + response.close(); + return contentLength == 0 ? CacheInfo.TOTAL_ERROR : contentLength; + } + } catch (IOException e) { + e.printStackTrace(); + } + return CacheInfo.TOTAL_ERROR; + } + + public void removeAllCall() { + for (String key : downCalls.keySet()) { + cancel(key); + } + } + + private List getAllFile() { + if (cacheDirectory.exists()) { + File[] files = cacheDirectory.listFiles(); + return Arrays.asList(files); + } + return new ArrayList<>(); + } + +} diff --git a/app/src/main/java/com/gh/download/cache/CacheObserver.java b/app/src/main/java/com/gh/download/cache/CacheObserver.java new file mode 100644 index 0000000000..357068d2f9 --- /dev/null +++ b/app/src/main/java/com/gh/download/cache/CacheObserver.java @@ -0,0 +1,32 @@ +package com.gh.download.cache; + +import com.lightgame.utils.Utils; + +import io.reactivex.Observer; +import io.reactivex.disposables.Disposable; + +public abstract class CacheObserver implements Observer { + protected Disposable d;//可以用于取消注册的监听者 + protected CacheInfo cacheInfo; + + @Override + public void onSubscribe(Disposable d) { + this.d = d; + } + + @Override + public void onNext(CacheInfo cacheInfo) { + this.cacheInfo = cacheInfo; + Utils.log(cacheInfo.getProgress() + "-" + cacheInfo.getTotal()); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onComplete() { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/video/detail/DetailPlayerView.kt b/app/src/main/java/com/gh/gamecenter/video/detail/DetailPlayerView.kt index e3a68c7ed2..70200e025a 100644 --- a/app/src/main/java/com/gh/gamecenter/video/detail/DetailPlayerView.kt +++ b/app/src/main/java/com/gh/gamecenter/video/detail/DetailPlayerView.kt @@ -7,6 +7,7 @@ import android.util.AttributeSet import android.view.Surface import android.view.View import android.widget.ImageView +import android.widget.SeekBar import androidx.core.content.ContextCompat import com.gh.common.constant.Constants import com.gh.common.util.* @@ -42,6 +43,7 @@ class DetailPlayerView @JvmOverloads constructor(context: Context, attrs: Attrib private var mDuration = 0 private var mRetry = false var isBottomContainerShow = false + private var byStartedClick = false //第一次进入不显示mBottomContainer override fun init(context: Context) { super.init(context) @@ -245,6 +247,13 @@ class DetailPlayerView @JvmOverloads constructor(context: Context, attrs: Attrib /********************************各类UI的状态显示*********************************************/ + override fun startAfterPrepared() { + super.startAfterPrepared() + setViewShowState(mStartButton, View.INVISIBLE) + setViewShowState(mBottomProgressBar, View.VISIBLE) + setViewShowState(mBottomContainer, View.GONE) + } + override fun hideAllWidget() { super.hideAllWidget() setViewShowState(mBottomProgressBar, View.VISIBLE) @@ -253,6 +262,7 @@ class DetailPlayerView @JvmOverloads constructor(context: Context, attrs: Attrib override fun changeUiToNormal() { super.changeUiToNormal() + byStartedClick = false setViewShowState(mStartButton, View.INVISIBLE) setViewShowState(mBottomProgressBar, View.VISIBLE) setViewShowState(mBottomContainer, View.GONE) @@ -269,8 +279,14 @@ class DetailPlayerView @JvmOverloads constructor(context: Context, attrs: Attrib override fun changeUiToPlayingShow() { super.changeUiToPlayingShow() - setViewShowState(mBottomProgressBar, if (isBottomContainerShow) View.GONE else View.VISIBLE) - setViewShowState(mBottomContainer, if (isBottomContainerShow) View.VISIBLE else View.GONE) + if (!byStartedClick) { + setViewShowState(mStartButton, View.INVISIBLE) + setViewShowState(mBottomContainer, View.GONE) + setViewShowState(mBottomProgressBar, View.VISIBLE) + } else { + setViewShowState(mBottomContainer, if (isBottomContainerShow) View.VISIBLE else View.GONE) + setViewShowState(mBottomProgressBar, if (isBottomContainerShow) View.GONE else View.VISIBLE) + } errorContainer.visibility = View.GONE } @@ -284,8 +300,13 @@ class DetailPlayerView @JvmOverloads constructor(context: Context, attrs: Attrib override fun changeUiToPlayingBufferingShow() { super.changeUiToPlayingBufferingShow() - setViewShowState(mBottomProgressBar, View.GONE) - setViewShowState(mBottomContainer, if (isBottomContainerShow) View.VISIBLE else View.GONE) + if (!byStartedClick) { + setViewShowState(mBottomProgressBar, View.VISIBLE) + setViewShowState(mBottomContainer, View.GONE) + } else { + setViewShowState(mBottomProgressBar, View.GONE) + setViewShowState(mBottomContainer, View.VISIBLE) + } } override fun changeUiToCompleteShow() { @@ -336,6 +357,7 @@ class DetailPlayerView @JvmOverloads constructor(context: Context, attrs: Attrib } override fun onClickUiToggle() { + byStartedClick = true //处理小于30s视频点击空白不能隐藏控件 if (mCurrentState == GSYVideoView.CURRENT_STATE_PLAYING) { if (mStartButton != null) { @@ -357,6 +379,11 @@ class DetailPlayerView @JvmOverloads constructor(context: Context, attrs: Attrib } } + override fun onStartTrackingTouch(seekBar: SeekBar?) { + super.onStartTrackingTouch(seekBar) + byStartedClick = true + } + override fun getEnlargeImageRes(): Int { return R.drawable.ic_game_detail_enter_full_screen } diff --git a/app/src/main/java/com/gh/gamecenter/video/detail/VideoAdapter.kt b/app/src/main/java/com/gh/gamecenter/video/detail/VideoAdapter.kt index 4e09f278cf..9ae85f7a16 100644 --- a/app/src/main/java/com/gh/gamecenter/video/detail/VideoAdapter.kt +++ b/app/src/main/java/com/gh/gamecenter/video/detail/VideoAdapter.kt @@ -5,6 +5,8 @@ import android.content.Context import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.gh.common.constant.Constants +import com.gh.download.cache.CacheManager +import com.gh.download.cache.CacheObserver import com.gh.gamecenter.entity.VideoEntity import com.halo.assistant.HaloApp import com.shuyu.gsyvideoplayer.builder.GSYVideoOptionBuilder @@ -72,6 +74,12 @@ class VideoAdapter(val mContext: Context, val mViewModel: VideoDetailContainerVi videoView.isNeedShowWifiTip = HaloApp.get(Constants.SHOULD_SHOW_VIDEO_MOBILE_WARNING, false) as Boolean? ?: false if (position == mViewModel.startPosition) { + if (position + 2 <= videoList.size - 1) { + CacheManager.getInstance().download(videoList[position + 1].url, object : CacheObserver() {}) + CacheManager.getInstance().download(videoList[position + 2].url, object : CacheObserver() {}) + } else if (position + 1 <= videoList.size - 1) {//预加载视频 + CacheManager.getInstance().download(videoList[position + 1].url, object : CacheObserver() {}) + } videoView.startButton.performClick() mViewModel.addHistoryRecord(videoList[position]) } diff --git a/app/src/main/java/com/gh/gamecenter/video/detail/VideoDetailContainerFragment.kt b/app/src/main/java/com/gh/gamecenter/video/detail/VideoDetailContainerFragment.kt index d89c748c1b..85f23c44a7 100644 --- a/app/src/main/java/com/gh/gamecenter/video/detail/VideoDetailContainerFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/video/detail/VideoDetailContainerFragment.kt @@ -14,6 +14,8 @@ import com.gh.common.util.* import com.gh.common.view.vertical_recycler.OnPagerListener import com.gh.common.view.vertical_recycler.PagerLayoutManager import com.gh.download.DownloadManager +import com.gh.download.cache.CacheManager +import com.gh.download.cache.CacheObserver import com.gh.gamecenter.R import com.gh.gamecenter.entity.VideoEntity import com.gh.gamecenter.manager.UserManager @@ -145,10 +147,17 @@ class VideoDetailContainerFragment : NormalFragment(), OnBackPressedListener { val pos = mViewPagerLayoutManager.findFirstCompletelyVisibleItemPosition() val videoView = findFirstCompletelyVisibleVideoViewByPosition() + if (pos + 2 <= mAdapter.videoList.size - 1) { + CacheManager.getInstance().download(mAdapter.videoList[pos + 1].url, object : CacheObserver() {}) + CacheManager.getInstance().download(mAdapter.videoList[pos + 2].url, object : CacheObserver() {}) + } else if (pos + 1 <= mAdapter.videoList.size - 1) {//预加载视频 + CacheManager.getInstance().download(mAdapter.videoList[pos + 1].url, object : CacheObserver() {}) + } if (videoView?.isInPlayingState != true) { + CacheManager.getInstance().cancel(mAdapter.videoList[pos].url) videoView?.startPlayLogic() mBaseHandler.postDelayed({ - videoView?.updateMuteStatus() + videoView?.startPlayLogic() }, 500) } mViewModel.addHistoryRecord(mAdapter.videoList[pos]) @@ -201,11 +210,20 @@ class VideoDetailContainerFragment : NormalFragment(), OnBackPressedListener { override fun onResume() { super.onResume() DownloadManager.getInstance(requireContext()).addObserver(dataWatcher) + CustomManager.onResumeAll() } override fun onPause() { super.onPause() DownloadManager.getInstance(requireContext()).removeObserver(dataWatcher) + CustomManager.onPauseAll() + } + + override fun onDestroyView() { + super.onDestroyView() + val uuid = (context as VideoDetailActivity).uuid + CustomManager.releaseAllVideos("detail_$uuid") + CacheManager.getInstance().removeAllCall() } private fun findFirstCompletelyVisibleVideoViewByPosition(): DetailPlayerView? {