初始化
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
group 'com.vincent.m3u8Downloader'
|
||||
version '1.0'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion 36
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 24
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
||||
@@ -0,0 +1 @@
|
||||
rootProject.name = 'm3u8Downloader'
|
||||
@@ -0,0 +1,3 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.vincent.m3u8Downloader">
|
||||
</manifest>
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package com.vincent.m3u8Downloader;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.AssetManager;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import io.flutter.FlutterInjector;
|
||||
import io.flutter.Log;
|
||||
import io.flutter.embedding.engine.FlutterEngine;
|
||||
import io.flutter.embedding.engine.dart.DartExecutor;
|
||||
import io.flutter.embedding.engine.loader.FlutterLoader;
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import io.flutter.plugin.common.JSONMethodCodec;
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.view.FlutterCallbackInformation;
|
||||
|
||||
/**
|
||||
* @Author: Vincent
|
||||
* @CreateAt: 2021/08/26 16:19
|
||||
* @Desc: 初始化运行回调调度程序的后台隔离,用于在后台启动时调用Dart回调。
|
||||
*/
|
||||
public class FlutterBackgroundExecutor implements MethodChannel.MethodCallHandler {
|
||||
public static final String SHARED_PREFERENCES_KEY = "vincent.m3u8.downloader.pref";
|
||||
public static final String CALLBACK_DISPATCHER_HANDLE_KEY = "callback_dispatcher_handle_key";
|
||||
private static final String TAG = "M3u8Downloader background";
|
||||
private MethodChannel backgroundChannel;
|
||||
private FlutterEngine backgroundFlutterEngine;
|
||||
private final AtomicBoolean isCallbackDispatcherReady = new AtomicBoolean(false);
|
||||
|
||||
public static void setCallbackDispatcher(Context context, long callbackHandle) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0);
|
||||
prefs.edit().putLong(CALLBACK_DISPATCHER_HANDLE_KEY, callbackHandle).apply();
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return isCallbackDispatcherReady.get();
|
||||
}
|
||||
|
||||
private void onInitialized() {
|
||||
isCallbackDispatcherReady.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
|
||||
String method = call.method;
|
||||
if (method.equals("didInitializeDispatcher")) {
|
||||
onInitialized();
|
||||
result.success(true);
|
||||
} else {
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
void startBackgroundIsolate(Context context) {
|
||||
if (!isRunning()) {
|
||||
SharedPreferences p = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0);
|
||||
long callbackHandle = p.getLong(CALLBACK_DISPATCHER_HANDLE_KEY, 0);
|
||||
startBackgroundIsolate(context, callbackHandle);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void startBackgroundIsolate(Context context, long callbackHandle) {
|
||||
if (backgroundFlutterEngine != null) {
|
||||
Log.e(TAG, "Background isolate already started");
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, "Starting Background isolate...");
|
||||
FlutterLoader flutterLoader = FlutterInjector.instance().flutterLoader();
|
||||
flutterLoader.startInitialization(context);
|
||||
flutterLoader.ensureInitializationComplete(context, null);
|
||||
String appBundlePath = flutterLoader.findAppBundlePath();
|
||||
AssetManager assets = context.getAssets();
|
||||
if (appBundlePath != null && !isRunning()) {
|
||||
backgroundFlutterEngine = new FlutterEngine(context);
|
||||
FlutterCallbackInformation flutterCallback = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle);
|
||||
if (flutterCallback == null) {
|
||||
Log.e(TAG, "Fatal: failed to find callback");
|
||||
return;
|
||||
}
|
||||
DartExecutor executor = backgroundFlutterEngine.getDartExecutor();
|
||||
initializeMethodChannel(executor);
|
||||
DartExecutor.DartCallback dartCallback = new DartExecutor.DartCallback(assets, appBundlePath, flutterCallback);
|
||||
|
||||
executor.executeDartCallback(dartCallback);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeMethodChannel(BinaryMessenger isolate) {
|
||||
backgroundChannel = new MethodChannel(isolate, "vincent/m3u8_downloader_background", JSONMethodCodec.INSTANCE);
|
||||
backgroundChannel.setMethodCallHandler(this);
|
||||
}
|
||||
|
||||
public void executeDartCallbackInBackgroundIsolate(long callbackHandle, Object args) {
|
||||
backgroundChannel.invokeMethod("", new Object[]{callbackHandle, args});
|
||||
}
|
||||
}
|
||||
+389
@@ -0,0 +1,389 @@
|
||||
package com.vincent.m3u8Downloader;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import io.flutter.Log;
|
||||
import com.vincent.m3u8Downloader.bean.M3U8;
|
||||
import com.vincent.m3u8Downloader.bean.M3U8Task;
|
||||
import com.vincent.m3u8Downloader.bean.M3U8TaskState;
|
||||
import com.vincent.m3u8Downloader.downloader.M3U8DownloadConfig;
|
||||
import com.vincent.m3u8Downloader.downloader.M3U8DownloadTask;
|
||||
import com.vincent.m3u8Downloader.downloader.M3U8Downloader;
|
||||
import com.vincent.m3u8Downloader.listener.OnM3U8DownloadListener;
|
||||
import com.vincent.m3u8Downloader.utils.M3U8Log;
|
||||
import com.vincent.m3u8Downloader.utils.M3U8Util;
|
||||
import com.vincent.m3u8Downloader.utils.NotificationUtil;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin;
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import io.flutter.plugin.common.JSONMethodCodec;
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
|
||||
import io.flutter.plugin.common.MethodChannel.Result;
|
||||
import io.flutter.plugin.common.PluginRegistry;
|
||||
|
||||
/** M3U8DownloaderPlugin */
|
||||
public class M3U8DownloaderPlugin implements FlutterPlugin, MethodCallHandler, PluginRegistry.NewIntentListener, ActivityAware {
|
||||
private static final String CHANNEL_NAME = "m3u8_downloader";
|
||||
|
||||
private MethodChannel channel;
|
||||
private Context context;
|
||||
private Activity mainActivity;
|
||||
private Handler handler;
|
||||
private final Object initializationLock = new Object();
|
||||
private boolean showNotification = true;
|
||||
private final FlutterBackgroundExecutor backgroundExecutor = new FlutterBackgroundExecutor();
|
||||
|
||||
private String fileName = "";
|
||||
private long progressCallbackHandle = -1;
|
||||
private long successCallbackHandle = -1;
|
||||
private long errorCallbackHandle = -1;
|
||||
|
||||
@Override
|
||||
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
|
||||
onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger());
|
||||
}
|
||||
|
||||
private void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger) {
|
||||
synchronized (initializationLock) {
|
||||
if (channel != null) {
|
||||
return;
|
||||
}
|
||||
this.context = applicationContext;
|
||||
handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
channel = new MethodChannel( messenger, CHANNEL_NAME, JSONMethodCodec.INSTANCE);
|
||||
channel.setMethodCallHandler(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) {
|
||||
String method = call.method;
|
||||
switch (method) {
|
||||
case "initialize":
|
||||
long callbackHandle = call.argument("handle");
|
||||
FlutterBackgroundExecutor.setCallbackDispatcher(context, callbackHandle);
|
||||
backgroundExecutor.startBackgroundIsolate(context);
|
||||
result.success(true);
|
||||
break;
|
||||
case "config":
|
||||
try {
|
||||
M3U8DownloadConfig config = M3U8DownloadConfig.build(context);
|
||||
if (call.hasArgument("saveDir") && call.argument("saveDir") != JSONObject.NULL) {
|
||||
String saveDir = call.argument("saveDir");
|
||||
config.setSaveDir(saveDir);
|
||||
}
|
||||
if (call.hasArgument("showNotification") && call.argument("showNotification") != JSONObject.NULL) {
|
||||
boolean show = call.argument("showNotification");
|
||||
showNotification = show;
|
||||
config.setShowNotification(show);
|
||||
}
|
||||
if (call.hasArgument("connTimeout") && call.argument("connTimeout") != JSONObject.NULL) {
|
||||
int connTimeout = call.argument("connTimeout");
|
||||
config.setConnTimeout(connTimeout);
|
||||
}
|
||||
if (call.hasArgument("readTimeout") && call.argument("readTimeout") != JSONObject.NULL) {
|
||||
int readTimeout = call.argument("readTimeout");
|
||||
config.setReadTimeout(readTimeout);
|
||||
}
|
||||
if (call.hasArgument("threadCount") && call.argument("threadCount") != JSONObject.NULL) {
|
||||
int threadCount = call.argument("threadCount");
|
||||
config.setThreadCount(threadCount);
|
||||
}
|
||||
if (call.hasArgument("debugMode") && call.argument("debugMode") != JSONObject.NULL) {
|
||||
boolean debugMode = call.argument("debugMode");
|
||||
config.setDebugMode(debugMode);
|
||||
}
|
||||
if (call.hasArgument("convertMp4") && call.argument("convertMp4") != JSONObject.NULL) {
|
||||
boolean convertMp4 = call.argument("convertMp4");
|
||||
config.setConvertMp4(convertMp4);
|
||||
}
|
||||
progressCallbackHandle = call.hasArgument("progressCallback") && call.argument("progressCallback") != JSONObject.NULL ? (long) call.argument("progressCallback") : -1;
|
||||
successCallbackHandle = call.hasArgument("successCallback") && call.argument("successCallback") != JSONObject.NULL ? (long) call.argument("successCallback") : -1;
|
||||
errorCallbackHandle = call.hasArgument("errorCallback") && call.argument("errorCallback") != JSONObject.NULL ? (long) call.argument("errorCallback") : -1;
|
||||
result.success(true);
|
||||
}catch (Error e){
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "download":
|
||||
if (!call.hasArgument("url")) {
|
||||
result.error("1", "url必传", "");
|
||||
return;
|
||||
}
|
||||
if (!call.hasArgument("name")) {
|
||||
result.error("1", "name必传", "");
|
||||
return;
|
||||
}
|
||||
showNotification = M3U8DownloadConfig.isShowNotification();
|
||||
String url = call.argument("url");
|
||||
if(M3U8Downloader.getInstance().isFinished(url)){
|
||||
|
||||
result.success("已下载完成了");
|
||||
return;
|
||||
}
|
||||
String currentTaskUrl = null;
|
||||
if(M3U8Downloader.getInstance().m3U8DownLoadTask != null){
|
||||
currentTaskUrl = M3U8Downloader.getInstance().m3U8DownLoadTask.m3u8Url;
|
||||
}
|
||||
if(currentTaskUrl != null
|
||||
&& currentTaskUrl.equals(url)
|
||||
&& M3U8Downloader.getInstance().m3U8DownLoadTask.isRunning()){
|
||||
result.success("正在执行");
|
||||
return;
|
||||
}
|
||||
fileName = call.argument("name");
|
||||
NotificationUtil.getInstance().cancel();
|
||||
if (showNotification) {
|
||||
NotificationUtil.getInstance().build(context);
|
||||
}
|
||||
|
||||
M3U8Downloader.getInstance().setOnM3U8DownloadListener(mDownloadListener);
|
||||
M3U8Downloader.getInstance().download(url);
|
||||
|
||||
result.success(null);
|
||||
break;
|
||||
case "searchInfo":
|
||||
if (!call.hasArgument("url")) {
|
||||
result.error("1", "url必传", "");
|
||||
return;
|
||||
}
|
||||
|
||||
String movieUrl = call.argument("url");
|
||||
Map infoMap = M3U8Downloader.getInstance().searchTaskInfo(movieUrl);
|
||||
result.success(infoMap);
|
||||
break;
|
||||
case "pause":
|
||||
// progressCallbackHandle = -1;
|
||||
// successCallbackHandle = -1;
|
||||
// errorCallbackHandle = -1;
|
||||
if (!call.hasArgument("url")) {
|
||||
result.error("1", "url必传", "");
|
||||
return;
|
||||
}
|
||||
String pauseUrl = call.argument("url");
|
||||
|
||||
M3U8Downloader.getInstance().pause(pauseUrl);
|
||||
result.success(null);
|
||||
break;
|
||||
case "pauseAll":
|
||||
// progressCallbackHandle = -1;
|
||||
// successCallbackHandle = -1;
|
||||
// errorCallbackHandle = -1;
|
||||
M3U8Downloader.getInstance().pauseAll();
|
||||
result.success(null);
|
||||
break;
|
||||
case "delete":
|
||||
if (!call.hasArgument("url")) {
|
||||
result.error("1", "url必传", "");
|
||||
return;
|
||||
}
|
||||
final String deleteUrl = call.argument("url");
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean flag = M3U8Downloader.getInstance().delete(deleteUrl);
|
||||
result.success(flag);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "deleteAll":
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean flag = M3U8Downloader.getInstance().deleteAll();
|
||||
result.success(flag);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "isRunning":
|
||||
result.success(M3U8Downloader.getInstance().isRunning());
|
||||
break;
|
||||
case "getSavePath":
|
||||
if (!call.hasArgument("url")) {
|
||||
result.error("1", "url必传", "");
|
||||
return;
|
||||
}
|
||||
String saveUrl = call.argument("url");
|
||||
Map<String, String> res = new HashMap<>();
|
||||
res.put("baseDir", M3U8Util.getSaveFileDir(saveUrl));
|
||||
res.put("baseDirTmp", M3U8Util.getSaveFileDirTmp(saveUrl));
|
||||
res.put("m3u8", M3U8DownloadTask.getM3U8Path(saveUrl));
|
||||
result.success(res);
|
||||
break;
|
||||
|
||||
default:
|
||||
result.notImplemented();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
|
||||
channel.setMethodCallHandler(null);
|
||||
}
|
||||
|
||||
final OnM3U8DownloadListener mDownloadListener = new OnM3U8DownloadListener() {
|
||||
@Override
|
||||
public void onDownloadPrepare(M3U8Task task) {
|
||||
if (showNotification) {
|
||||
NotificationUtil.getInstance().updateNotification(fileName, M3U8TaskState.PREPARE, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadPending(M3U8Task task) {
|
||||
if (showNotification) {
|
||||
NotificationUtil.getInstance().updateNotification(fileName, M3U8TaskState.PENDING, Math.round(task.getProgress() * 100));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadProgress(M3U8DownloadTask task) {
|
||||
|
||||
if (progressCallbackHandle != -1) {
|
||||
Log.e("load proress callback:", "!=-1");
|
||||
final Map<String, Object> args = new HashMap<>();
|
||||
args.put("url", task.m3u8Url);
|
||||
args.put("progress", task.progress());
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.e("load proress callback run:", "");
|
||||
backgroundExecutor.executeDartCallbackInBackgroundIsolate(progressCallbackHandle, args);
|
||||
}
|
||||
});
|
||||
}else {
|
||||
Log.e("load proress callback:", "==-1");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadItem(M3U8Task task, long itemFileSize, int totalTs, int curTs) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadSuccess(M3U8DownloadTask task) {
|
||||
; if (showNotification) {
|
||||
NotificationUtil.getInstance().updateNotification(fileName, M3U8TaskState.SUCCESS, 100);
|
||||
}
|
||||
String saveDir = M3U8Util.getSaveFileDir(task.m3u8Url);
|
||||
String filePath = "";
|
||||
if (task.m3u8Url != null) {
|
||||
filePath = task.currentM3U8.getLocalPath();
|
||||
}
|
||||
final Map<String, Object> args = new HashMap<>();
|
||||
args.put("url", task.m3u8Url);
|
||||
args.put("dir", saveDir);
|
||||
args.put("filePath", filePath);
|
||||
|
||||
//下载成功
|
||||
if (successCallbackHandle != -1) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
backgroundExecutor.executeDartCallbackInBackgroundIsolate(successCallbackHandle, args);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadPause(M3U8Task task) {
|
||||
if (showNotification) {
|
||||
NotificationUtil.getInstance().updateNotification(fileName, M3U8TaskState.PAUSE, Math.round(task.getProgress() * 100));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConvert() {
|
||||
if (showNotification) {
|
||||
NotificationCompat.Builder builder = NotificationUtil.getInstance().getBuilder();
|
||||
if (builder == null) return;
|
||||
|
||||
builder.setContentText("正在转成MP4")
|
||||
.setProgress(100, 100, true)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download);
|
||||
NotificationManagerCompat.from(context).notify(NotificationUtil.NOTIFICATION_ID, builder.build());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadError(String url, Throwable error) {
|
||||
|
||||
if (errorCallbackHandle != -1) {
|
||||
final Map<String, Object> args = new HashMap<>();
|
||||
args.put("url", url);
|
||||
if(error != null) {
|
||||
args.put("error", error.getMessage());
|
||||
}else {
|
||||
args.put("error", "no reason");
|
||||
}
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
backgroundExecutor.executeDartCallbackInBackgroundIsolate(errorCallbackHandle, args);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(M3U8Task task) {
|
||||
if (showNotification) {
|
||||
NotificationUtil.getInstance().cancel();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean onNewIntent(Intent intent) {
|
||||
if (NotificationUtil.ACTION_SELECT_NOTIFICATION.equals(intent.getAction())) {
|
||||
M3U8Log.d("selectNotification");
|
||||
channel.invokeMethod("selectNotification", null);
|
||||
if (mainActivity != null) {
|
||||
mainActivity.setIntent(intent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
|
||||
binding.addOnNewIntentListener(this);
|
||||
mainActivity = binding.getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromActivityForConfigChanges() {
|
||||
this.mainActivity = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
|
||||
onAttachedToActivity(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromActivity() {
|
||||
this.mainActivity = null;
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package com.vincent.m3u8Downloader.bean;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author: Vincent
|
||||
* @CreateAt: 2021/08/25 16:31
|
||||
* @Desc: m3u8实体类
|
||||
*/
|
||||
public class M3U8 {
|
||||
private String baseUrl;
|
||||
private String dirPath;
|
||||
private String localPath;
|
||||
private String key;
|
||||
private String iv;
|
||||
public String methodCode;
|
||||
public String methodKeyURL;
|
||||
public int localTSFileCount = 0;
|
||||
|
||||
private List<M3U8Ts> tsList = new ArrayList<>();
|
||||
|
||||
public String getBaseUrl() {
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public String getDirPath() {
|
||||
return dirPath;
|
||||
}
|
||||
|
||||
public void setDirPath(String dirPath) {
|
||||
this.dirPath = dirPath;
|
||||
}
|
||||
|
||||
public String getLocalPath() {
|
||||
return localPath;
|
||||
}
|
||||
|
||||
public void setLocalPath(String localPath) {
|
||||
this.localPath = localPath;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getIv() {
|
||||
return iv;
|
||||
}
|
||||
|
||||
public void setIv(String iv) {
|
||||
this.iv = iv;
|
||||
}
|
||||
|
||||
public List<M3U8Ts> getTsList() {
|
||||
return tsList;
|
||||
}
|
||||
|
||||
public void setTsList(List<M3U8Ts> tsList) {
|
||||
this.tsList = tsList;
|
||||
}
|
||||
|
||||
public void addTs(M3U8Ts ts) {
|
||||
this.tsList.add(ts);
|
||||
}
|
||||
|
||||
public long getTotalFileSize() {
|
||||
long fileSize = 0;
|
||||
for (M3U8Ts m3U8Ts : tsList){
|
||||
fileSize = fileSize + m3U8Ts.getFileSize();
|
||||
}
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
public long getTotalTime() {
|
||||
long totalTime = 0;
|
||||
for (M3U8Ts m3U8Ts : tsList){
|
||||
totalTime = totalTime + (int)(m3U8Ts.getSeconds() * 1000);
|
||||
}
|
||||
return totalTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "M3U8{" +
|
||||
"basePath='" + baseUrl + '\'' +
|
||||
", dirPath='" + dirPath + '\'' +
|
||||
", localPath='" + localPath + '\'' +
|
||||
", key='" + key + '\'' +
|
||||
", iv='" + iv + '\'' +
|
||||
", totalFileSize=" + getTotalFileSize() +
|
||||
", totalTime=" + getTotalTime() +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
M3U8 m3U8 = (M3U8) o;
|
||||
return baseUrl.equals(m3U8.baseUrl);
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package com.vincent.m3u8Downloader.bean;
|
||||
|
||||
import com.vincent.m3u8Downloader.utils.M3U8Util;
|
||||
|
||||
/**
|
||||
* @Author: Vincent
|
||||
* @CreateAt: 2021/08/25 17:20
|
||||
* @Desc: M3U8下载任务
|
||||
*/
|
||||
public class M3U8Task {
|
||||
|
||||
private String url;
|
||||
private M3U8TaskState state = M3U8TaskState.DEFAULT;
|
||||
private long speed;
|
||||
private float progress;
|
||||
private M3U8 m3U8;
|
||||
|
||||
private M3U8Task() {}
|
||||
|
||||
public M3U8Task(String url){
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public M3U8TaskState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(M3U8TaskState state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public long getSpeed() {
|
||||
return speed;
|
||||
}
|
||||
|
||||
public void setSpeed(long speed) {
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
public float getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
public void setProgress(float progress) {
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
public M3U8 getM3U8() {
|
||||
return m3U8;
|
||||
}
|
||||
|
||||
public void setM3U8(M3U8 m3U8) {
|
||||
this.m3U8 = m3U8;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
M3U8Task m3U8Task = (M3U8Task) o;
|
||||
return url.equals(m3U8Task.url);
|
||||
}
|
||||
|
||||
public String getFormatSpeed() {
|
||||
if (speed == 0) return "";
|
||||
return M3U8Util.formatFileSize(speed) + "/s";
|
||||
}
|
||||
|
||||
public long getTotalSize() {
|
||||
if (m3U8 == null) return 0;
|
||||
return m3U8.getTotalFileSize();
|
||||
}
|
||||
|
||||
public String getFormatTotalSize() {
|
||||
if (m3U8 == null) return "";
|
||||
long fileSize = getTotalSize();
|
||||
if (fileSize == 0) return "";
|
||||
return M3U8Util.formatFileSize(fileSize);
|
||||
}
|
||||
|
||||
public String getFormatCurrentSize() {
|
||||
if (m3U8 == null)return "";
|
||||
return M3U8Util.formatFileSize((long)(progress * m3U8.getTotalFileSize()));
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.vincent.m3u8Downloader.bean;
|
||||
|
||||
/**
|
||||
* @Author: Vincent
|
||||
* @CreateAt: 2021/08/25 17:18
|
||||
* @Desc: 下载任务状态
|
||||
*/
|
||||
public enum M3U8TaskState {
|
||||
/**
|
||||
* 默认状态
|
||||
*/
|
||||
DEFAULT,
|
||||
/**
|
||||
* 下载排队中
|
||||
*/
|
||||
PENDING,
|
||||
/**
|
||||
* 下载准备中
|
||||
*/
|
||||
PREPARE,
|
||||
/**
|
||||
* 正在下载中
|
||||
*/
|
||||
DOWNLOADING,
|
||||
/**
|
||||
* 下载成功
|
||||
*/
|
||||
SUCCESS,
|
||||
/**
|
||||
* 下载失败
|
||||
*/
|
||||
ERROR,
|
||||
/**
|
||||
* 暂停下载
|
||||
*/
|
||||
PAUSE,
|
||||
/**
|
||||
* 存储空间不足
|
||||
*/
|
||||
ENOSPC,
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package com.vincent.m3u8Downloader.bean;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.vincent.m3u8Downloader.utils.EncryptUtil;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
|
||||
|
||||
/**
|
||||
* @Author: Vincent
|
||||
* @CreateAt: 2021/08/25 16:36
|
||||
* @Desc: m3u8切片
|
||||
*/
|
||||
public class M3U8Ts implements Comparable<M3U8Ts> {
|
||||
/**
|
||||
* ts网络请求地址(完整的网络请求地址请使用obtainFullUrl)
|
||||
*/
|
||||
private String url;
|
||||
/**
|
||||
* 文件大小
|
||||
*/
|
||||
private long fileSize;
|
||||
/**
|
||||
* ts秒数
|
||||
*/
|
||||
private float seconds;
|
||||
|
||||
public M3U8Ts(String url, float seconds) {
|
||||
this.url = url;
|
||||
this.seconds = seconds;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public long getFileSize() {
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
public void setFileSize(long fileSize) {
|
||||
this.fileSize = fileSize;
|
||||
}
|
||||
|
||||
public float getSeconds() {
|
||||
return seconds;
|
||||
}
|
||||
|
||||
public void setSeconds(float seconds) {
|
||||
this.seconds = seconds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(M3U8Ts m3U8Ts) {
|
||||
return url.compareTo(m3U8Ts.url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "M3U8Ts{" +
|
||||
"url='" + url + '\'' +
|
||||
", fileSize=" + fileSize +
|
||||
", seconds=" + seconds +
|
||||
'}';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加密后的文件名
|
||||
* @return ts文件名
|
||||
*/
|
||||
public String obtainEncodeTsFileName(){
|
||||
if (url == null) return "error.ts";
|
||||
String fileName = EncryptUtil.md5Encode(url).concat(".ts");
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完整的URL地址
|
||||
* @param hostUrl host地址
|
||||
* @return URL地址
|
||||
*/
|
||||
public URL obtainFullUrl(String hostUrl) throws MalformedURLException {
|
||||
if (url == null || hostUrl == null) {
|
||||
return null;
|
||||
}
|
||||
URL host = new URL(hostUrl);
|
||||
return new URL(host, url);
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package com.vincent.m3u8Downloader.downloader;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Environment;
|
||||
|
||||
import com.vincent.m3u8Downloader.utils.SpHelper;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* @Author: Vincent
|
||||
* @CreateAt: 2021/08/25 21:37
|
||||
* @Desc: 配置类
|
||||
*/
|
||||
public class M3U8DownloadConfig {
|
||||
private static final String TAG_SAVE_DIR = "TAG_SAVE_DIR_M3U8";
|
||||
private static final String TAG_THREAD_COUNT = "TAG_THREAD_COUNT_M3U8";
|
||||
private static final String TAG_CONN_TIMEOUT = "TAG_CONN_TIMEOUT_M3U8";
|
||||
private static final String TAG_READ_TIMEOUT = "TAG_READ_TIMEOUT_M3U8";
|
||||
private static final String TAG_DEBUG = "TAG_DEBUG_M3U8";
|
||||
private static final String TAG_SHOW_NOTIFICATION = "TAG_SHOW_NOTIFICATION_M3U8";
|
||||
private static final String TAG_CONVERT_MP4 = "TAG_CONVERT_MP4";
|
||||
|
||||
public static M3U8DownloadConfig build(Context context){
|
||||
SpHelper.init(context);
|
||||
return new M3U8DownloadConfig();
|
||||
}
|
||||
|
||||
public M3U8DownloadConfig setSaveDir(String saveDir){
|
||||
SpHelper.putString(TAG_SAVE_DIR, saveDir);
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public static String getSaveDir(){
|
||||
return SpHelper.getString(TAG_SAVE_DIR, Environment.getExternalStorageDirectory().getPath() + File.separator + "M3u8Downloader");
|
||||
}
|
||||
|
||||
public M3U8DownloadConfig setThreadCount(int threadCount){
|
||||
if (threadCount > 5) threadCount = 5;
|
||||
if (threadCount <= 0) threadCount = 1;
|
||||
SpHelper.putInt(TAG_THREAD_COUNT, threadCount);
|
||||
return this;
|
||||
}
|
||||
|
||||
public static int getThreadCount(){
|
||||
return SpHelper.getInt(TAG_THREAD_COUNT, 3);
|
||||
}
|
||||
|
||||
public M3U8DownloadConfig setConnTimeout(int connTimeout){
|
||||
SpHelper.putInt(TAG_CONN_TIMEOUT, connTimeout);
|
||||
return this;
|
||||
}
|
||||
|
||||
public static int getConnTimeout(){
|
||||
return SpHelper.getInt(TAG_CONN_TIMEOUT, 10 * 1000);
|
||||
}
|
||||
|
||||
public M3U8DownloadConfig setReadTimeout(int readTimeout){
|
||||
SpHelper.putInt(TAG_READ_TIMEOUT, readTimeout);
|
||||
return this;
|
||||
}
|
||||
|
||||
public static int getReadTimeout(){
|
||||
return SpHelper.getInt(TAG_READ_TIMEOUT, 30 * 60 * 1000);
|
||||
}
|
||||
|
||||
|
||||
public M3U8DownloadConfig setDebugMode(boolean debug){
|
||||
SpHelper.putBoolean(TAG_DEBUG, debug);
|
||||
return this;
|
||||
}
|
||||
|
||||
public static boolean isDebugMode(){
|
||||
return SpHelper.getBoolean(TAG_DEBUG, false);
|
||||
}
|
||||
|
||||
public M3U8DownloadConfig setShowNotification(boolean show){
|
||||
SpHelper.putBoolean(TAG_SHOW_NOTIFICATION, show);
|
||||
return this;
|
||||
}
|
||||
|
||||
public static boolean isShowNotification(){
|
||||
return SpHelper.getBoolean(TAG_SHOW_NOTIFICATION, true);
|
||||
}
|
||||
|
||||
public M3U8DownloadConfig setConvertMp4(boolean convertMp4){
|
||||
SpHelper.putBoolean(TAG_CONVERT_MP4, convertMp4);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
package com.vincent.m3u8Downloader.downloader;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.vincent.m3u8Downloader.bean.M3U8;
|
||||
import com.vincent.m3u8Downloader.bean.M3U8Ts;
|
||||
import com.vincent.m3u8Downloader.listener.OnInfoCallback;
|
||||
import com.vincent.m3u8Downloader.listener.OnM3U8DownloadListener;
|
||||
import com.vincent.m3u8Downloader.utils.M3U8Log;
|
||||
import com.vincent.m3u8Downloader.utils.M3U8Util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* @Author: Vincent
|
||||
* @CreateAt: 2021/08/25 22:26
|
||||
* @Desc: M3U8下载任务
|
||||
*/
|
||||
|
||||
|
||||
public class M3U8DownloadTask {
|
||||
|
||||
public static final String LOCAL_FILE_NAME = "local.m3u8";
|
||||
public static final String REMOTE_FILE_NAME = "remote.m3u8";
|
||||
public static final String M3U8_KEY_NAME = "key.key";
|
||||
|
||||
private static final int WHAT_ON_ERROR = 1001;
|
||||
private static final int WHAT_ON_PROGRESS = 1002;
|
||||
private static final int WHAT_ON_SUCCESS = 1003;
|
||||
private static final int WHAT_ON_START_DOWNLOAD = 1004;
|
||||
private static final int WHAT_ON_CONVERT = 1005;
|
||||
|
||||
|
||||
// 文件保存地址
|
||||
public String m3u8Url;
|
||||
// 文件保存地址
|
||||
private String saveDir;
|
||||
// 当前M3U8
|
||||
public M3U8 currentM3U8;
|
||||
// 线程池
|
||||
private ExecutorService executor;
|
||||
|
||||
// 任务是否正在运行
|
||||
private boolean isRunning = false;
|
||||
// 当前已经在下完成的大小
|
||||
private long curLength = 0;
|
||||
// 当前下载完成的文件个数
|
||||
private final AtomicInteger curTs = new AtomicInteger(0);
|
||||
// 总文件个数
|
||||
private volatile int totalTs = 0;
|
||||
// 单个文件的大小
|
||||
private volatile long itemFileSize = 0;
|
||||
// 下载任务监听器
|
||||
int connTimeout;
|
||||
int readTimeout;
|
||||
int threadCount;
|
||||
|
||||
|
||||
public float progress(){
|
||||
if (totalTs <= 0){
|
||||
return 0;
|
||||
}else {
|
||||
return (float) (curTs.get() * 100.0/totalTs);
|
||||
}
|
||||
}
|
||||
|
||||
private final WeakHandler mHandler = new WeakHandler(new Handler.Callback() {
|
||||
@Override
|
||||
public boolean handleMessage(Message msg) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
public M3U8DownloadTask() {
|
||||
connTimeout = M3U8DownloadConfig.getConnTimeout();
|
||||
readTimeout = M3U8DownloadConfig.getReadTimeout();
|
||||
threadCount = M3U8DownloadConfig.getThreadCount();
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return isRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始下载
|
||||
* @param url m3u8下载地址
|
||||
* @param onTaskDownloadListener 任务下载监听器
|
||||
*/
|
||||
public void download(final String url, final OnM3U8DownloadListener onTaskDownloadListener) {
|
||||
m3u8Url = url;
|
||||
saveDir = M3U8Util.getSaveFileDirTmp(url);
|
||||
File dirTmp = new File(saveDir);
|
||||
if(!dirTmp.exists()){
|
||||
if(!dirTmp.mkdir()){
|
||||
M3U8Log.e( "fail:" + dirTmp.getPath());
|
||||
}
|
||||
}
|
||||
isRunning = true;
|
||||
getM3U8Info(url, new OnInfoCallback() {// 获取m3u8
|
||||
@Override
|
||||
public void success(final M3U8 m3u8) {
|
||||
currentM3U8 = m3u8;
|
||||
start(m3u8, onTaskDownloadListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(Exception e) {
|
||||
isRunning = false;
|
||||
currentM3U8 = null;
|
||||
onTaskDownloadListener.onDownloadError(url, e.getCause());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取m3u8信息
|
||||
* @param url m3u8地址
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
private synchronized void getM3U8Info(final String url, final OnInfoCallback callback) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
M3U8 m3u8 = M3U8Util.parseIndex(url, saveDir + File.separator + REMOTE_FILE_NAME);
|
||||
curTs.set(m3u8.localTSFileCount);
|
||||
totalTs = m3u8.getTsList().size();
|
||||
callback.success(m3u8);
|
||||
} catch (Exception e) {
|
||||
callback.error(e);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始下载
|
||||
*/
|
||||
private void start(final M3U8 m3u8Model, final OnM3U8DownloadListener onTaskDownloadListener) {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
batchDownloadTs(m3u8Model, onTaskDownloadListener);// 开始下载
|
||||
if (isRunning) {
|
||||
String m3u8Path = saveDir + File.separator + LOCAL_FILE_NAME;
|
||||
if (TextUtils.isEmpty(currentM3U8.methodKeyURL)) {
|
||||
M3U8Util.createLocalM3U8(m3u8Path, currentM3U8);
|
||||
} else {
|
||||
M3U8Util.createLocalM3U8(m3u8Path, currentM3U8, M3U8_KEY_NAME, currentM3U8.getIv());
|
||||
}
|
||||
|
||||
File file = new File(saveDir);
|
||||
if (file.isDirectory()){
|
||||
String finishPath = M3U8Util.getSaveFileDir(m3u8Url);
|
||||
if(file.renameTo(new File(finishPath))){
|
||||
currentM3U8.setLocalPath(finishPath + File.separator + LOCAL_FILE_NAME);
|
||||
currentM3U8.setDirPath(finishPath);
|
||||
}else{
|
||||
M3U8Log.e("movie cache==文件替换失败" + file.getPath());
|
||||
}
|
||||
}
|
||||
isRunning = false;
|
||||
onTaskDownloadListener.onDownloadSuccess(M3U8DownloadTask.this);
|
||||
}
|
||||
} catch (InterruptedIOException e) {
|
||||
// 被中断了,使用stop时会抛出这个,不需要处理
|
||||
isRunning = false;
|
||||
onTaskDownloadListener.onDownloadError(m3u8Url, e.getCause());
|
||||
} catch (IOException e) {
|
||||
isRunning = false;
|
||||
onTaskDownloadListener.onDownloadError(m3u8Url, e.getCause());
|
||||
handlerError(e);
|
||||
} catch (Exception e) {
|
||||
isRunning = false;
|
||||
onTaskDownloadListener.onDownloadError(m3u8Url, e.getCause());
|
||||
handlerError(e);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载ts切片
|
||||
* @param m3u8Info M3U8对象
|
||||
*/
|
||||
private void batchDownloadTs(final M3U8 m3u8Info, final OnM3U8DownloadListener onTaskDownloadListener) {
|
||||
final File dir = new File(saveDir);
|
||||
if (!dir.exists()) {
|
||||
dir.mkdirs();
|
||||
}
|
||||
if (!TextUtils.isEmpty(m3u8Info.getKey())) {
|
||||
try {// 保存key文件
|
||||
M3U8Util.saveFile(m3u8Info.getKey(), saveDir + File.separator + "key.key");
|
||||
} catch (IOException e) {
|
||||
M3U8Log.e("saveFile fail!" + e.getMessage());
|
||||
handlerError(e);
|
||||
}
|
||||
}
|
||||
M3U8Log.d("Downloading !");
|
||||
final String basePath = m3u8Info.getBaseUrl();
|
||||
for (final M3U8Ts m3u8Ts : m3u8Info.getTsList()) {
|
||||
if (!isRunning){
|
||||
return;
|
||||
}
|
||||
netTaskLoad(m3u8Ts, dir, basePath, onTaskDownloadListener);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void netTaskLoad(M3U8Ts m3u8Ts, File dir, String basePath, final OnM3U8DownloadListener onTaskDownloadListener){
|
||||
File file;
|
||||
String fileName = dir + File.separator + m3u8Ts.obtainEncodeTsFileName();
|
||||
try {
|
||||
file = new File(fileName);
|
||||
} catch (Exception e) {
|
||||
file = new File(dir + File.separator + m3u8Ts.getUrl());
|
||||
}
|
||||
|
||||
if (!file.exists()) {
|
||||
M3U8Log.d("ts load url=========" + m3u8Ts.getUrl());
|
||||
FileOutputStream fos = null;
|
||||
InputStream inputStream = null;
|
||||
boolean readFinished = false;
|
||||
try {
|
||||
URL url = m3u8Ts.obtainFullUrl(basePath);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setConnectTimeout(connTimeout);
|
||||
conn.setReadTimeout(readTimeout);
|
||||
if (conn.getResponseCode() == 200) {
|
||||
inputStream = conn.getInputStream();
|
||||
File tmpFile = new File(fileName + "_tmp");
|
||||
if(tmpFile.exists()){
|
||||
tmpFile.delete();
|
||||
}
|
||||
fos = new FileOutputStream(tmpFile);//会自动创建文件
|
||||
int len;
|
||||
byte[] buf = new byte[1024];
|
||||
while ((len = inputStream.read(buf)) != -1) {
|
||||
curLength += len;
|
||||
fos.write(buf, 0, len);//写入流中
|
||||
}
|
||||
fos.close();
|
||||
if(!tmpFile.renameTo(file)){
|
||||
M3U8Log.e("rename file fail:" + tmpFile.getPath());
|
||||
}
|
||||
} else {
|
||||
handlerError(new Throwable("ts:" + url + "netCode:" + conn.getResponseCode()));
|
||||
}
|
||||
readFinished = true;
|
||||
} catch (MalformedURLException e) {
|
||||
M3U8Log.e("MalformedURLException" + e.getMessage());
|
||||
handlerError(e);
|
||||
} catch (IOException e) {
|
||||
M3U8Log.e("IOException" + e.getMessage());
|
||||
handlerError(e);
|
||||
} catch (Error e) {
|
||||
M3U8Log.e("Error=" + e.getMessage());
|
||||
} finally {
|
||||
// 如果没有读取完,则删除
|
||||
if (!readFinished && file.exists()) {
|
||||
file.delete();
|
||||
}
|
||||
// 关流
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (fos != null) {
|
||||
try {
|
||||
fos.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
itemFileSize = file.length();
|
||||
m3u8Ts.setFileSize(itemFileSize);
|
||||
curTs.incrementAndGet();
|
||||
onTaskDownloadListener.onDownloadProgress(this);
|
||||
}
|
||||
|
||||
} else {
|
||||
itemFileSize = file.length();
|
||||
m3u8Ts.setFileSize(itemFileSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止任务
|
||||
*/
|
||||
public void stop() {
|
||||
M3U8Log.d("=========task stop");
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理异常
|
||||
* @param e 异常信息
|
||||
*/
|
||||
private void handlerError(Throwable e) {
|
||||
if (!"Task running".equals(e.getMessage())) {
|
||||
// stop();
|
||||
}
|
||||
// 不提示被中断的情况
|
||||
if ("thread interrupted".equals(e.getMessage())) {
|
||||
return;
|
||||
}
|
||||
e.printStackTrace();
|
||||
// Message msg = Message.obtain();
|
||||
// msg.obj = e;
|
||||
// msg.what = WHAT_ON_ERROR;
|
||||
// mHandler.sendMessage(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取m3u8本地路径
|
||||
* @param url m3u8地址
|
||||
* @return 文件路径
|
||||
*/
|
||||
public static String getM3U8Path(String url) {
|
||||
return M3U8Util.getSaveFileDir(url) + File.separator + LOCAL_FILE_NAME;
|
||||
}
|
||||
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
package com.vincent.m3u8Downloader.downloader;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.vincent.m3u8Downloader.bean.M3U8;
|
||||
import com.vincent.m3u8Downloader.listener.OnM3U8DownloadListener;
|
||||
import com.vincent.m3u8Downloader.listener.OnTaskDownloadListener;
|
||||
import com.vincent.m3u8Downloader.utils.M3U8Log;
|
||||
import com.vincent.m3u8Downloader.utils.M3U8Util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
|
||||
|
||||
/**
|
||||
* @Author: Vincent
|
||||
* @CreateAt: 2021/08/25 22:17
|
||||
* @Desc: M3U8下载器
|
||||
*/
|
||||
public class M3U8Downloader {
|
||||
private static M3U8Downloader instance;
|
||||
|
||||
public M3U8DownloadTask m3U8DownLoadTask;
|
||||
private OnM3U8DownloadListener onM3U8DownloadListener;
|
||||
private long currentTime;
|
||||
|
||||
private M3U8Downloader() {
|
||||
|
||||
}
|
||||
public static M3U8Downloader getInstance(){
|
||||
if (null == instance) {
|
||||
instance = new M3U8Downloader();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void setOnM3U8DownloadListener(OnM3U8DownloadListener onM3U8DownloadListener) {
|
||||
this.onM3U8DownloadListener = onM3U8DownloadListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 防止快速点击引起ThreadPoolExecutor频繁创建销毁引起crash
|
||||
* @return 是否快速点击
|
||||
*/
|
||||
private boolean isQuicklyClick(){
|
||||
boolean result = false;
|
||||
if (System.currentTimeMillis() - currentTime <= 100){
|
||||
result = true;
|
||||
M3U8Log.d("is too quickly click!");
|
||||
}
|
||||
currentTime = System.currentTimeMillis();
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean isFinished(String url){
|
||||
String dirPath = M3U8Util.getSaveFileDir(url);
|
||||
File file = new File(dirPath);
|
||||
File mp4File = new File(dirPath + "/loacl.mp4");
|
||||
if(file.exists() && file.isDirectory()){
|
||||
if (!mp4File.exists()){
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public HashMap<String, String> searchTaskInfo(String url) {
|
||||
|
||||
HashMap<String, String> infoMap = new HashMap<String, String>();
|
||||
String currentTaskUrl = null;
|
||||
if(m3U8DownLoadTask != null
|
||||
&& m3U8DownLoadTask.isRunning()){
|
||||
currentTaskUrl = m3U8DownLoadTask.m3u8Url;
|
||||
}
|
||||
|
||||
if(currentTaskUrl != null && currentTaskUrl.equals(url)){
|
||||
infoMap.put("isLoaderRunning", "1");
|
||||
}else {
|
||||
infoMap.put("isLoaderRunning", "0");
|
||||
M3U8Log.d("searchTaskInfo url===" + currentTaskUrl);
|
||||
M3U8Log.d("searchTaskInfo url===" + url);
|
||||
}
|
||||
|
||||
File file = new File(M3U8Util.getSaveFileDir(url));
|
||||
|
||||
if(currentTaskUrl != null && currentTaskUrl.equals(url)){
|
||||
double progress = m3U8DownLoadTask.progress();
|
||||
infoMap.put("progress", String.format("%.2f", progress));
|
||||
}else if(file.exists() && file.isDirectory()){
|
||||
infoMap.put("progress", "100.00");
|
||||
infoMap.put("localPath", M3U8Util.getSaveFileDir(url) + File.separator + M3U8DownloadTask.LOCAL_FILE_NAME);
|
||||
}else {
|
||||
File tmpDir = new File(M3U8Util.getSaveFileDirTmp(url));
|
||||
if(tmpDir.exists() && tmpDir.isDirectory()){
|
||||
String m3u8InfoFilePath = M3U8Util.getSaveFileDirTmp(url) + File.separator + M3U8DownloadTask.REMOTE_FILE_NAME;
|
||||
File m3u8InfoFile = new File(m3u8InfoFilePath);
|
||||
if (m3u8InfoFile.exists()){
|
||||
try {
|
||||
M3U8 m3u8 = M3U8Util.parseIndex(url, m3u8InfoFilePath);
|
||||
File[] files = tmpDir.listFiles();
|
||||
int allTaskCount = m3u8.getTsList().size() + 3;
|
||||
int finishCount = files.length;
|
||||
if(allTaskCount > 3){
|
||||
double progress = finishCount*100.0/allTaskCount;
|
||||
infoMap.put("progress", String.format("%.2f", progress));
|
||||
}else {
|
||||
infoMap.put("progress", "0.00");
|
||||
infoMap.put("error", "m3u8 文件解析错误");
|
||||
}
|
||||
}catch (Error | IOException e){
|
||||
infoMap.put("progress", "0.00");
|
||||
infoMap.put("error", "m3u8 文件异常");
|
||||
}
|
||||
}else {
|
||||
infoMap.put("progress", "0.00");
|
||||
}
|
||||
}else {
|
||||
infoMap.put("progress", "0.00");
|
||||
}
|
||||
}
|
||||
return infoMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载m3u8
|
||||
* @param url m3u8下载地址
|
||||
*/
|
||||
public void download(String url) {
|
||||
if (TextUtils.isEmpty(url) || isQuicklyClick()) return;
|
||||
|
||||
// 暂停之前的
|
||||
if(m3U8DownLoadTask != null){
|
||||
m3U8DownLoadTask.stop();
|
||||
m3U8DownLoadTask = null;
|
||||
}
|
||||
|
||||
// 开启新的下载
|
||||
m3U8DownLoadTask = new M3U8DownloadTask();
|
||||
|
||||
try {
|
||||
M3U8Log.d("start downloading: " + url);
|
||||
m3U8DownLoadTask.download(url, onM3U8DownloadListener);
|
||||
} catch (Exception e){
|
||||
e.printStackTrace();
|
||||
M3U8Log.e("startDownloadTask Error:"+e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 暂停任务(非当前任务)
|
||||
*/
|
||||
public void pause(String url){
|
||||
M3U8Log.d("pause download: " + url);
|
||||
if(m3U8DownLoadTask != null && m3U8DownLoadTask.m3u8Url.equals(url)){
|
||||
m3U8DownLoadTask.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void pauseAll(){
|
||||
if(m3U8DownLoadTask != null){
|
||||
m3U8DownLoadTask.stop();
|
||||
m3U8DownLoadTask = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 删除下载文件。非线程安全
|
||||
* @param url 下载地址
|
||||
* @return 删除状态
|
||||
*/
|
||||
public boolean delete(final String url){
|
||||
if (m3U8DownLoadTask != null && m3U8DownLoadTask.m3u8Url.equals(url)) {
|
||||
m3U8DownLoadTask.stop();
|
||||
m3U8DownLoadTask = null;
|
||||
}
|
||||
String saveDir = M3U8Util.getSaveFileDir(url);
|
||||
String saveDirTmp = M3U8Util.getSaveFileDirTmp(url);
|
||||
// 删除文件夹
|
||||
boolean isDelete = M3U8Util.clearDir(new File(saveDir));
|
||||
boolean isDeleteTmp = M3U8Util.clearDir(new File(saveDirTmp));
|
||||
return isDelete && isDeleteTmp;
|
||||
}
|
||||
public boolean deleteAll() {
|
||||
if (m3U8DownLoadTask != null) {
|
||||
m3U8DownLoadTask.stop();
|
||||
}
|
||||
String saveDir = M3U8Util.getSaveDir();
|
||||
boolean isDelete = M3U8Util.clearDir(new File(saveDir));
|
||||
return isDelete;
|
||||
}
|
||||
/**
|
||||
* 是否正在下载
|
||||
* @return 运行状态
|
||||
*/
|
||||
public boolean isRunning(){
|
||||
return m3U8DownLoadTask.isRunning();
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载任务监听器
|
||||
*/
|
||||
private final OnTaskDownloadListener onTaskDownloadListener = new OnTaskDownloadListener() {
|
||||
private long lastLength;
|
||||
private float downloadProgress;
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
// currentM3U8Task.setState(M3U8TaskState.PREPARE);
|
||||
// if (onM3U8DownloadListener != null){
|
||||
// onM3U8DownloadListener.onDownloadPrepare(currentM3U8Task);
|
||||
// }
|
||||
// M3U8Log.d("onDownloadPrepare: "+ currentM3U8Task.getUrl());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartDownload(int totalTs, int curTs) {
|
||||
M3U8Log.d("onStartDownload: "+totalTs+"|"+curTs);
|
||||
//
|
||||
// currentM3U8Task.setState(M3U8TaskState.DOWNLOADING);
|
||||
// if (totalTs > 0) {
|
||||
// downloadProgress = 1.0f * curTs / totalTs;
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadItem(long itemFileSize, int totalTs, int curTs) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(M3U8DownloadTask tas) {
|
||||
// if (onM3U8DownloadListener != null && m3U8DownLoadTask != null){
|
||||
// currentM3U8Task.setProgress(tas.progress());
|
||||
// onM3U8DownloadListener.onDownloadProgress(currentM3U8Task);
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConvert() {
|
||||
M3U8Log.d("onConvert!");
|
||||
if (onM3U8DownloadListener != null){
|
||||
onM3U8DownloadListener.onConvert();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onSuccess(M3U8 m3U8) {
|
||||
M3U8Log.d("m3u8 Downloader onSuccess: "+ m3U8);
|
||||
m3U8DownLoadTask.stop();
|
||||
// currentM3U8Task.setM3U8(m3U8);
|
||||
// currentM3U8Task.setState(M3U8TaskState.SUCCESS);
|
||||
// if (onM3U8DownloadListener != null) {
|
||||
// onM3U8DownloadListener.onDownloadSuccess(currentM3U8Task);
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
// if (error.getMessage() != null && error.getMessage().contains("ENOSPC")){
|
||||
// currentM3U8Task.setState(M3U8TaskState.ENOSPC);
|
||||
// }else {
|
||||
// currentM3U8Task.setState(M3U8TaskState.ERROR);
|
||||
// }
|
||||
// if (onM3U8DownloadListener != null) {
|
||||
// onM3U8DownloadListener.onDownloadError(currentM3U8Task.getUrl(), error);
|
||||
// }
|
||||
// M3U8Log.e("onError: " + error.getMessage());
|
||||
}
|
||||
};
|
||||
}
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
package com.vincent.m3u8Downloader.downloader;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Memory safer implementation of android.os.Handler
|
||||
* <p/>
|
||||
* Original implementation of Handlers always keeps hard reference to handler in queue of execution.
|
||||
* If you create anonymous handler and post delayed message into it, it will keep all parent class
|
||||
* for that time in memory even if it could be cleaned.
|
||||
* <p/>
|
||||
* This implementation is trickier, it will keep WeakReferences to runnables and messages,
|
||||
* and GC could collect them once WeakHandler instance is not referenced any more
|
||||
* <p/>
|
||||
*
|
||||
* @see Handler
|
||||
*
|
||||
* Created by Dmytro Voronkevych on 17/06/2014.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class WeakHandler {
|
||||
private final Handler.Callback mCallback; // hard reference to Callback. We need to keep callback in memory
|
||||
private final ExecHandler mExec;
|
||||
private Lock mLock = new ReentrantLock();
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@VisibleForTesting
|
||||
final ChainedRef mRunnables = new ChainedRef(mLock, null);
|
||||
|
||||
public WeakHandler() {
|
||||
mCallback = null;
|
||||
mExec = new ExecHandler();
|
||||
}
|
||||
|
||||
public WeakHandler(@Nullable Handler.Callback callback) {
|
||||
mCallback = callback; // Hard referencing body
|
||||
mExec = new ExecHandler(new WeakReference<>(callback)); // Weak referencing inside ExecHandler
|
||||
}
|
||||
|
||||
public WeakHandler(@NonNull Looper looper) {
|
||||
mCallback = null;
|
||||
mExec = new ExecHandler(looper);
|
||||
}
|
||||
|
||||
|
||||
public WeakHandler(@NonNull Looper looper, @NonNull Handler.Callback callback) {
|
||||
mCallback = callback;
|
||||
mExec = new ExecHandler(looper, new WeakReference<>(callback));
|
||||
}
|
||||
|
||||
|
||||
public final boolean post(@NonNull Runnable r) {
|
||||
return mExec.post(wrapRunnable(r));
|
||||
}
|
||||
|
||||
|
||||
public final boolean postAtTime(@NonNull Runnable r, long uptimeMillis) {
|
||||
return mExec.postAtTime(wrapRunnable(r), uptimeMillis);
|
||||
}
|
||||
|
||||
|
||||
public final boolean postAtTime(Runnable r, Object token, long uptimeMillis) {
|
||||
return mExec.postAtTime(wrapRunnable(r), token, uptimeMillis);
|
||||
}
|
||||
|
||||
|
||||
public final boolean postDelayed(Runnable r, long delayMillis) {
|
||||
return mExec.postDelayed(wrapRunnable(r), delayMillis);
|
||||
}
|
||||
|
||||
public final boolean postAtFrontOfQueue(Runnable r) {
|
||||
return mExec.postAtFrontOfQueue(wrapRunnable(r));
|
||||
}
|
||||
|
||||
public final void removeCallbacks(Runnable r) {
|
||||
final WeakRunnable runnable = mRunnables.remove(r);
|
||||
if (runnable != null) {
|
||||
mExec.removeCallbacks(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public final void removeCallbacks(Runnable r, Object token) {
|
||||
final WeakRunnable runnable = mRunnables.remove(r);
|
||||
if (runnable != null) {
|
||||
mExec.removeCallbacks(runnable, token);
|
||||
}
|
||||
}
|
||||
|
||||
public final boolean sendMessage(Message msg) {
|
||||
return mExec.sendMessage(msg);
|
||||
}
|
||||
|
||||
|
||||
public final boolean sendEmptyMessage(int what) {
|
||||
return mExec.sendEmptyMessage(what);
|
||||
}
|
||||
|
||||
|
||||
public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
|
||||
return mExec.sendEmptyMessageDelayed(what, delayMillis);
|
||||
}
|
||||
|
||||
public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) {
|
||||
return mExec.sendEmptyMessageAtTime(what, uptimeMillis);
|
||||
}
|
||||
|
||||
public final boolean sendMessageDelayed(Message msg, long delayMillis) {
|
||||
return mExec.sendMessageDelayed(msg, delayMillis);
|
||||
}
|
||||
|
||||
|
||||
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
|
||||
return mExec.sendMessageAtTime(msg, uptimeMillis);
|
||||
}
|
||||
|
||||
|
||||
public final boolean sendMessageAtFrontOfQueue(Message msg) {
|
||||
return mExec.sendMessageAtFrontOfQueue(msg);
|
||||
}
|
||||
|
||||
|
||||
public final void removeMessages(int what) {
|
||||
mExec.removeMessages(what);
|
||||
}
|
||||
|
||||
|
||||
public final void removeMessages(int what, Object object) {
|
||||
mExec.removeMessages(what, object);
|
||||
}
|
||||
|
||||
|
||||
public final void removeCallbacksAndMessages(Object token) {
|
||||
mExec.removeCallbacksAndMessages(token);
|
||||
}
|
||||
|
||||
public final boolean hasMessages(int what) {
|
||||
return mExec.hasMessages(what);
|
||||
}
|
||||
|
||||
public final boolean hasMessages(int what, Object object) {
|
||||
return mExec.hasMessages(what, object);
|
||||
}
|
||||
|
||||
public final Looper getLooper() {
|
||||
return mExec.getLooper();
|
||||
}
|
||||
|
||||
private WeakRunnable wrapRunnable(@NonNull Runnable r) {
|
||||
//noinspection ConstantConditions
|
||||
if (r == null) {
|
||||
throw new NullPointerException("Runnable can't be null");
|
||||
}
|
||||
final ChainedRef hardRef = new ChainedRef(mLock, r);
|
||||
mRunnables.insertAfter(hardRef);
|
||||
return hardRef.wrapper;
|
||||
}
|
||||
|
||||
private static class ExecHandler extends Handler {
|
||||
private final WeakReference<Callback> mCallback;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
ExecHandler() {
|
||||
mCallback = null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
ExecHandler(WeakReference<Callback> callback) {
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
ExecHandler(Looper looper) {
|
||||
super(looper);
|
||||
mCallback = null;
|
||||
}
|
||||
|
||||
ExecHandler(Looper looper, WeakReference<Callback> callback) {
|
||||
super(looper);
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(@NonNull Message msg) {
|
||||
if (mCallback == null) {
|
||||
return;
|
||||
}
|
||||
final Callback callback = mCallback.get();
|
||||
if (callback == null) { // Already disposed
|
||||
return;
|
||||
}
|
||||
callback.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
static class WeakRunnable implements Runnable {
|
||||
private final WeakReference<Runnable> mDelegate;
|
||||
private final WeakReference<ChainedRef> mReference;
|
||||
|
||||
WeakRunnable(WeakReference<Runnable> delegate, WeakReference<ChainedRef> reference) {
|
||||
mDelegate = delegate;
|
||||
mReference = reference;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
final Runnable delegate = mDelegate.get();
|
||||
final ChainedRef reference = mReference.get();
|
||||
if (reference != null) {
|
||||
reference.remove();
|
||||
}
|
||||
if (delegate != null) {
|
||||
delegate.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class ChainedRef {
|
||||
@Nullable
|
||||
ChainedRef next;
|
||||
@Nullable
|
||||
ChainedRef prev;
|
||||
@NonNull
|
||||
final Runnable runnable;
|
||||
@NonNull
|
||||
final WeakRunnable wrapper;
|
||||
|
||||
@NonNull
|
||||
Lock lock;
|
||||
|
||||
public ChainedRef(@NonNull Lock lock, @NonNull Runnable r) {
|
||||
this.runnable = r;
|
||||
this.lock = lock;
|
||||
this.wrapper = new WeakRunnable(new WeakReference<>(r), new WeakReference<>(this));
|
||||
}
|
||||
|
||||
public WeakRunnable remove() {
|
||||
lock.lock();
|
||||
try {
|
||||
if (prev != null) {
|
||||
prev.next = next;
|
||||
}
|
||||
if (next != null) {
|
||||
next.prev = prev;
|
||||
}
|
||||
prev = null;
|
||||
next = null;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
public void insertAfter(@NonNull ChainedRef candidate) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (this.next != null) {
|
||||
this.next.prev = candidate;
|
||||
}
|
||||
|
||||
candidate.next = this.next;
|
||||
this.next = candidate;
|
||||
candidate.prev = this;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public WeakRunnable remove(Runnable obj) {
|
||||
lock.lock();
|
||||
try {
|
||||
ChainedRef curr = this.next; // Skipping head
|
||||
while (curr != null) {
|
||||
if (curr.runnable == obj) { // We do comparison exactly how Handler does inside
|
||||
return curr.remove();
|
||||
}
|
||||
curr = curr.next;
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.vincent.m3u8Downloader.listener;
|
||||
|
||||
import com.vincent.m3u8Downloader.bean.M3U8;
|
||||
|
||||
/**
|
||||
* @Author: Vincent
|
||||
* @CreateAt: 2021/08/26 9:58
|
||||
* @Desc: 获取M3U8文件信息的回调函数
|
||||
*/
|
||||
public interface OnInfoCallback {
|
||||
|
||||
void success(M3U8 m3u8);
|
||||
void error(Exception e);
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package com.vincent.m3u8Downloader.listener;
|
||||
|
||||
import com.vincent.m3u8Downloader.bean.M3U8Task;
|
||||
import com.vincent.m3u8Downloader.downloader.M3U8DownloadTask;
|
||||
|
||||
/**
|
||||
* @Author: Vincent
|
||||
* @CreateAt: 2021/08/25 22:40
|
||||
* @Desc: M3U8Downloader 监听器
|
||||
*/
|
||||
public interface OnM3U8DownloadListener {
|
||||
|
||||
/**
|
||||
* 下载准备
|
||||
* @param task 当前准备任务
|
||||
*/
|
||||
void onDownloadPrepare(M3U8Task task);
|
||||
|
||||
/**
|
||||
* 等待下载
|
||||
* @param task 等待的任务
|
||||
*/
|
||||
void onDownloadPending(M3U8Task task);
|
||||
|
||||
/**
|
||||
* 下载进度
|
||||
* 异步回调,不可以直接在UI线程调用
|
||||
* @param task 当前下载任务
|
||||
*/
|
||||
void onDownloadProgress(M3U8DownloadTask task);
|
||||
|
||||
/**
|
||||
* 完成一次下载任务
|
||||
* @param task 下载任务
|
||||
* @param itemFileSize 此任务的文件大小
|
||||
* @param totalTs 总切片数
|
||||
* @param curTs 已下载切片数
|
||||
*/
|
||||
void onDownloadItem(M3U8Task task, long itemFileSize, int totalTs, int curTs);
|
||||
|
||||
/**
|
||||
* 下载成功
|
||||
*/
|
||||
void onDownloadSuccess(M3U8DownloadTask task);
|
||||
|
||||
/**
|
||||
* 暂停下载
|
||||
* @param task 暂停的任务
|
||||
*/
|
||||
void onDownloadPause(M3U8Task task);
|
||||
|
||||
/**
|
||||
* 准备转成MP4
|
||||
*/
|
||||
void onConvert();
|
||||
|
||||
/**
|
||||
* 下载失败
|
||||
* @param url 失败的任务
|
||||
* @param error 错误信息
|
||||
*/
|
||||
void onDownloadError(String url, final Throwable error);
|
||||
|
||||
/**
|
||||
* 停止下载
|
||||
* @param task 停止的任务
|
||||
*/
|
||||
void onStop(M3U8Task task);
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.vincent.m3u8Downloader.listener;
|
||||
|
||||
import com.vincent.m3u8Downloader.bean.M3U8;
|
||||
import com.vincent.m3u8Downloader.downloader.M3U8DownloadTask;
|
||||
|
||||
/**
|
||||
* @Author: Vincent
|
||||
* @CreateAt: 2021/08/25 22:37
|
||||
* @Desc: 任务下载监听器
|
||||
*/
|
||||
public interface OnTaskDownloadListener {
|
||||
|
||||
/**
|
||||
* 开始任务
|
||||
*/
|
||||
void onStart();
|
||||
|
||||
/**
|
||||
* 开始下载
|
||||
* @param totalTs ts总数
|
||||
* @param curTs 当前下载完成的ts个数
|
||||
*/
|
||||
void onStartDownload(int totalTs, int curTs);
|
||||
|
||||
/**
|
||||
* ts文件下载完成
|
||||
* 注意:这个方法是异步的(子线程中执行),所以不能在此方法中回调,其他方法为主线程中回调
|
||||
* @param itemFileSize 单个文件的大小
|
||||
* @param totalTs ts总数
|
||||
* @param curTs 当前下载完成的ts个数
|
||||
*/
|
||||
void onDownloadItem(long itemFileSize, int totalTs, int curTs);
|
||||
|
||||
/**
|
||||
* 定时进度
|
||||
* @param task
|
||||
*/
|
||||
void onProgress(M3U8DownloadTask task);
|
||||
|
||||
/**
|
||||
* 正在转成MP4格式
|
||||
*/
|
||||
void onConvert();
|
||||
|
||||
/**
|
||||
* 下载成功
|
||||
*/
|
||||
void onSuccess(M3U8 m3U8);
|
||||
|
||||
/**
|
||||
* 错误的时候回调
|
||||
* 线程环境无法保证,不可以直接在UI线程调用
|
||||
* @param error 错误信息
|
||||
*/
|
||||
void onError(Throwable error);
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
package com.vincent.m3u8Downloader.utils;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* @Author: Vincent
|
||||
* @CreateAt: 2021/08/25 16:42
|
||||
* @Desc: 加解密工具
|
||||
*/
|
||||
public class EncryptUtil {
|
||||
|
||||
private final static String ENCODING = "UTF-8";
|
||||
|
||||
/**
|
||||
* md5加密字符串
|
||||
* @param str 待加密字符串
|
||||
* @return 加密后的字符串
|
||||
*/
|
||||
public static String md5Encode(String str) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
md.update(str.getBytes());
|
||||
return new BigInteger(1, md.digest()).toString(16);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成密钥
|
||||
* 自动生成base64 编码后的AES128位密钥
|
||||
*/
|
||||
public static String getAESKey() throws Exception {
|
||||
KeyGenerator kg = KeyGenerator.getInstance("AES");
|
||||
kg.init(128);
|
||||
SecretKey sk = kg.generateKey();
|
||||
byte[] b = sk.getEncoded();
|
||||
return parseByte2HexStr(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* AES 加密
|
||||
* @param base64Key base64编码后的 AES key
|
||||
* @param text 待加密的字符串
|
||||
* @return 加密后的byte[]
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
public static byte[] getAESEncode(String base64Key, String text) throws Exception{
|
||||
return getAESEncode(base64Key, text.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* AES 加密
|
||||
* @param base64Key base64编码后的 AES key
|
||||
* @param bytes 待加密的bytes
|
||||
* @return 加密后的byte[]
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
public static byte[] getAESEncode(String base64Key, byte[] bytes) throws Exception{
|
||||
if (base64Key == null)return bytes;
|
||||
byte[] key = parseHexStr2Byte(base64Key);
|
||||
SecretKeySpec sKeySpec = new SecretKeySpec(key, "AES");
|
||||
Cipher cipher = Cipher.getInstance("AES");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, sKeySpec);
|
||||
return cipher.doFinal(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* AES解密
|
||||
* @param base64Key base64编码后的 AES key
|
||||
* @param text 待解密的字符串
|
||||
* @return 解密后的byte[]
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
public static byte[] getAESDecode(String base64Key, String text) throws Exception{
|
||||
return getAESDecode(base64Key, text.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* AES解密
|
||||
* @param base64Key base64编码后的 AES key
|
||||
* @param bytes 待解密的字符串
|
||||
* @return 解密后的byte[] 数组
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
public static byte[] getAESDecode(String base64Key, byte[] bytes) throws Exception{
|
||||
if (base64Key == null)return bytes;
|
||||
byte[] key = parseHexStr2Byte(base64Key);
|
||||
SecretKeySpec sKeySpec = new SecretKeySpec(key, "AES");
|
||||
Cipher cipher = Cipher.getInstance("AES");
|
||||
cipher.init(Cipher.DECRYPT_MODE, sKeySpec);
|
||||
return cipher.doFinal(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将二进制转换成16进制
|
||||
* @param buf byte数组
|
||||
* @return 16进制字符串
|
||||
*/
|
||||
public static String parseByte2HexStr(byte[] buf) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : buf) {
|
||||
String hex = Integer.toHexString(b & 0xFF);
|
||||
if (hex.length() == 1) {
|
||||
hex = '0' + hex;
|
||||
}
|
||||
sb.append(hex.toUpperCase());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将16进制转换为二进制
|
||||
* @param hexStr 16进制字符串
|
||||
* @return byte[]
|
||||
*/
|
||||
public static byte[] parseHexStr2Byte(String hexStr) {
|
||||
if (hexStr.length() < 1)
|
||||
return null;
|
||||
byte[] result = new byte[hexStr.length()/2];
|
||||
for (int i = 0; i< hexStr.length()/2; i++) {
|
||||
int high = Integer.parseInt(hexStr.substring(i*2, i*2+1), 16);
|
||||
int low = Integer.parseInt(hexStr.substring(i*2+1, i*2+2), 16);
|
||||
result[i] = (byte) (high * 16 + low);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密ts文件
|
||||
* @param bytes 文件字节流
|
||||
* @param key m3u8的key
|
||||
* @param iv m3u8的iv
|
||||
* @return 解密后的byte[]
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
public static byte[] decryptTs(byte[] bytes, String key, String iv) throws Exception {
|
||||
if (TextUtils.isEmpty(key)) {
|
||||
return bytes;
|
||||
}
|
||||
if(key.length() != 16){
|
||||
String utf8Key = key.getBytes(ENCODING).toString();
|
||||
M3U8Log.e("key 长度不是16位--" + key.length() + "--" + utf8Key + "--" + utf8Key.length());
|
||||
if (utf8Key.length() == 16) {
|
||||
key = utf8Key;
|
||||
}else {
|
||||
// return bytes;
|
||||
}
|
||||
}
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
|
||||
|
||||
byte[] ivByte = new byte[16];
|
||||
if (!TextUtils.isEmpty(iv)) {
|
||||
if (iv.startsWith("0x"))
|
||||
ivByte = parseHexStr2Byte(iv.substring(2));
|
||||
else
|
||||
ivByte = iv.getBytes();
|
||||
|
||||
if (ivByte == null || ivByte.length != 16)
|
||||
ivByte = new byte[16];
|
||||
}
|
||||
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(ENCODING), "AES");
|
||||
AlgorithmParameterSpec paramSpec = new IvParameterSpec(ivByte);
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec);
|
||||
return cipher.doFinal(bytes);
|
||||
}
|
||||
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.vincent.m3u8Downloader.utils;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.vincent.m3u8Downloader.downloader.M3U8DownloadConfig;
|
||||
|
||||
/**
|
||||
* @Author: Vincent
|
||||
* @CreateAt: 2021/08/25 17:58
|
||||
* @Desc: M3U8日志系统
|
||||
*/
|
||||
public class M3U8Log {
|
||||
private static final boolean isDebugMode = M3U8DownloadConfig.isDebugMode();
|
||||
private static final String TAG = "M3U8Log";
|
||||
private static final String PREFIX = "M3U8Log cache error:====== ";
|
||||
|
||||
public static void d(String msg) {
|
||||
if (isDebugMode) Log.d(TAG, TAG + msg);
|
||||
}
|
||||
|
||||
public static void e(String msg) {
|
||||
if (isDebugMode) Log.e(TAG, PREFIX + msg);
|
||||
}
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
package com.vincent.m3u8Downloader.utils;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import com.vincent.m3u8Downloader.downloader.M3U8DownloadConfig;
|
||||
import com.vincent.m3u8Downloader.bean.M3U8;
|
||||
import com.vincent.m3u8Downloader.bean.M3U8Ts;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* @Author: Vincent
|
||||
* @CreateAt: 2021/08/25 21:43
|
||||
* @Desc: M3U8工具类
|
||||
*/
|
||||
public class M3U8Util {
|
||||
|
||||
/**
|
||||
* 将Url转换为M3U8对象
|
||||
* @param url url地址
|
||||
* @return M3U8对象
|
||||
* @throws IOException IO异常
|
||||
*/
|
||||
public static M3U8 parseIndex(String url, String filePath) throws IOException {
|
||||
M3U8 ret = new M3U8();
|
||||
URL baseUrl = new URL(url);
|
||||
File remoteFile = new File(filePath);
|
||||
BufferedReader reader;
|
||||
Boolean fromLocalFile = false;
|
||||
if (remoteFile.exists() && remoteFile.length() > 100) {
|
||||
File dir = new File(remoteFile.getParent());
|
||||
if (dir.isDirectory()) {
|
||||
int count = dir.listFiles().length - 1;
|
||||
if (count > 0){
|
||||
ret.localTSFileCount = count;
|
||||
}
|
||||
}
|
||||
fromLocalFile = true;
|
||||
reader = new BufferedReader(new FileReader(filePath));
|
||||
}else {
|
||||
reader = new BufferedReader(new InputStreamReader(baseUrl.openStream()));
|
||||
}
|
||||
|
||||
ret.setBaseUrl(url);
|
||||
StringBuilder strBuilder = new StringBuilder();
|
||||
String line;
|
||||
float seconds = 0;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (!fromLocalFile) {
|
||||
strBuilder.append(line + "\n");
|
||||
}
|
||||
if (line.startsWith("#")) {
|
||||
if (line.startsWith("#EXTINF:")) {
|
||||
line = line.substring(8);
|
||||
if (line.endsWith(",")) {
|
||||
line = line.substring(0, line.length() - 1);
|
||||
}
|
||||
seconds = Float.parseFloat(line);
|
||||
} else if (line.startsWith("#EXT-X-KEY:")) {
|
||||
String[] lineInfoArr = line.split("#EXT-X-KEY:");
|
||||
line = lineInfoArr[1];
|
||||
String[] arr = line.split(",");
|
||||
for (String s : arr) {
|
||||
if (s.contains("=")) {
|
||||
int eqIndex = s.indexOf("=");
|
||||
String k = s.substring(0, eqIndex);
|
||||
// 取第一个 '=' 之后的全部,不能用 split("=")[1]:
|
||||
// key 地址常带 query(?token=xxx),split 会把地址截成半截
|
||||
String v = s.substring(eqIndex + 1);
|
||||
if (k.equals("URI")) {
|
||||
// 获取key。引号统一在这里去掉,留着的话上层 createLocalM3U8
|
||||
// 再包一层就成了 URI=""http..."",取不出地址
|
||||
v = v.replace("\"", "");
|
||||
if (v.length() > 0 && !v.startsWith("http")) {
|
||||
// 相对地址按 m3u8 自身地址补全(可能是同目录相对路径,不能只拼 scheme+host)
|
||||
try {
|
||||
v = new URL(baseUrl, v).toString();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
ret.methodKeyURL = v;
|
||||
// if(Thread.currentThread() != Looper.getMainLooper().getThread()){
|
||||
// // 只有查询信息才走主线程, key 本地化
|
||||
// BufferedReader keyReader = new BufferedReader(new InputStreamReader(new URL(baseUrl, v).openStream()));
|
||||
// ret.setKey(keyReader.readLine());
|
||||
// M3U8Log.d("m3u8 key: " + ret.getKey());
|
||||
// }
|
||||
} else if (k.equals("IV")) {
|
||||
// 获取IV
|
||||
ret.setIv(v);
|
||||
M3U8Log.d("m3u8 IV: " + v);
|
||||
}else if(k.equals("METHOD")){
|
||||
ret.methodCode = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (line.endsWith("m3u8")) {
|
||||
return parseIndex(new URL(baseUrl, line).toString(), filePath + "sub.m3u8");
|
||||
}
|
||||
ret.addTs(new M3U8Ts(line, seconds));
|
||||
seconds = 0;
|
||||
}
|
||||
reader.close();
|
||||
if(!fromLocalFile) {
|
||||
BufferedWriter bfw = new BufferedWriter(new FileWriter(filePath, false));
|
||||
bfw.write(strBuilder.toString());
|
||||
bfw.close();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成AES-128加密本地m3u8索引文件,ts切片和m3u8文件放在相同目录下即可
|
||||
* @param m3U8 m3u8文件
|
||||
* @param keyPath 加密key
|
||||
*/
|
||||
public static void createLocalM3U8(String fileName, M3U8 m3U8, String keyPath, String iv) throws IOException{
|
||||
M3U8Log.d("createLocalM3U8: " + fileName);
|
||||
String basePath = M3U8Util.getSaveFileDir(m3U8.getBaseUrl());
|
||||
BufferedWriter bfw = new BufferedWriter(new FileWriter(fileName, false));
|
||||
bfw.write("#EXTM3U\n");
|
||||
bfw.write("#EXT-X-VERSION:3\n");
|
||||
bfw.write("#EXT-X-MEDIA-SEQUENCE:0\n");
|
||||
bfw.write("#EXT-X-TARGETDURATION:13\n");
|
||||
if (keyPath != null) {
|
||||
String keyContent = "#EXT-X-KEY:METHOD=" + m3U8.methodCode + ",URI=" ;
|
||||
// keyContent = keyContent + basePath + "/" + keyPath + "\"";
|
||||
keyContent = keyContent + "\"" + m3U8.methodKeyURL + "\"";
|
||||
if(iv != null){
|
||||
keyContent = keyContent + "," + "IV=" + iv;
|
||||
}
|
||||
keyContent = keyContent + "\n";
|
||||
bfw.write(keyContent);
|
||||
}
|
||||
for (M3U8Ts m3U8Ts : m3U8.getTsList()) {
|
||||
bfw.write("#EXTINF:" + m3U8Ts.getSeconds()+",\n");
|
||||
bfw.write( "file://" + basePath + "/" + m3U8Ts.obtainEncodeTsFileName());
|
||||
// M3U8Log.d("file://" + basePath + "/" + m3U8Ts.obtainEncodeTsFileName());
|
||||
bfw.newLine();
|
||||
}
|
||||
bfw.write("#EXT-X-ENDLIST");
|
||||
bfw.flush();
|
||||
bfw.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空文件夹
|
||||
* @param dir 文件夹/文件地址
|
||||
* @return 删除状态
|
||||
*/
|
||||
public static boolean clearDir(File dir) {
|
||||
if (dir.exists()) {
|
||||
if (dir.isFile()) {
|
||||
return dir.delete();
|
||||
} else if (dir.isDirectory()) {
|
||||
File[] files = dir.listFiles();
|
||||
if (files != null && files.length > 0) {
|
||||
for (File file : files) {
|
||||
clearDir(file);
|
||||
}
|
||||
}
|
||||
return dir.delete();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private static final float KB = 1024;
|
||||
private static final float MB = 1024 * KB;
|
||||
private static final float GB = 1024 * MB;
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
* @param size 文件大小
|
||||
* @return 格式化字符串
|
||||
*/
|
||||
@SuppressLint("DefaultLocale")
|
||||
public static String formatFileSize(long size){
|
||||
if (size >= GB) {
|
||||
return String.format("%.1f GB", size / GB);
|
||||
} else if (size >= MB) {
|
||||
float value = size / MB;
|
||||
return String.format(value > 100 ? "%.0f MB" : "%.1f MB", value);
|
||||
} else if (size >= KB) {
|
||||
float value = size / KB;
|
||||
return String.format(value > 100 ? "%.0f KB" : "%.1f KB", value);
|
||||
} else {
|
||||
return String.format("%d B", size);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成本地m3u8索引文件,ts切片和m3u8文件放在相同目录下即可
|
||||
* @param m3U8 m3u8文件
|
||||
*/
|
||||
public static void createLocalM3U8(String fileName, M3U8 m3U8) throws IOException{
|
||||
createLocalM3U8(fileName, m3U8, null, null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 保存文件
|
||||
* @param text 文件内容
|
||||
* @param fileName 文件名
|
||||
* @throws IOException IO异常
|
||||
*/
|
||||
public static void saveFile(String text, String fileName) throws IOException{
|
||||
File file = new File(fileName);
|
||||
BufferedWriter out = new BufferedWriter(new FileWriter(file));
|
||||
out.write(text);
|
||||
out.flush();
|
||||
out.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取url保存的地址
|
||||
* @param url 请求地址
|
||||
* @return 地址
|
||||
*/
|
||||
public static String getSaveFileDir(String url){
|
||||
String readUrl = Uri.parse(url).getPath();
|
||||
Log.d("M3U8::", "getSaveFileDir:" + url);
|
||||
Log.d("M3U8::", "getSaveFileDir->readUrl" + readUrl);
|
||||
return M3U8DownloadConfig.getSaveDir() + File.separator + EncryptUtil.md5Encode(readUrl);
|
||||
}
|
||||
public static String getSaveFileDirTmp(String url){
|
||||
String readUrl = Uri.parse(url).getPath();
|
||||
return M3U8DownloadConfig.getSaveDir() + File.separator + EncryptUtil.md5Encode(readUrl) + "_tmp";
|
||||
}
|
||||
|
||||
public static String getSaveDir(){
|
||||
return M3U8DownloadConfig.getSaveDir();
|
||||
}
|
||||
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
package com.vincent.m3u8Downloader.utils;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import com.vincent.m3u8Downloader.bean.M3U8TaskState;
|
||||
|
||||
/**
|
||||
* @Author: Vincent
|
||||
* @CreateAt: 2021/08/26 16:47
|
||||
* @Desc:
|
||||
*/
|
||||
public class NotificationUtil {
|
||||
public static final int NOTIFICATION_ID = 9527;
|
||||
public static final String NOTIFICATION_CHANNEL_ID = "M3U8_DOWNLOADER_NOTIFICATION";
|
||||
public static final String ACTION_SELECT_NOTIFICATION = "SELECT_NOTIFICATION";
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private static NotificationUtil instance;
|
||||
private NotificationCompat.Builder builder;
|
||||
private android.app.NotificationManager notificationManager;
|
||||
private Context context;
|
||||
private int notificationProgress = -100;
|
||||
|
||||
public static NotificationUtil getInstance(){
|
||||
if (null == instance) {
|
||||
instance = new NotificationUtil();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建通知
|
||||
* @param c 上下文
|
||||
*/
|
||||
public void build(Context c) {
|
||||
if (notificationManager != null) return;
|
||||
this.context = c;
|
||||
|
||||
// Make a channel if necessary
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// Create the NotificationChannel, but only on API 26+ because
|
||||
// the NotificationChannel class is new and not in the support library
|
||||
|
||||
CharSequence name = context.getApplicationInfo().loadLabel(context.getPackageManager());
|
||||
int importance = android.app.NotificationManager.IMPORTANCE_DEFAULT;
|
||||
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance);
|
||||
channel.setSound(null, null);
|
||||
|
||||
// Add the channel
|
||||
notificationManager = context.getSystemService(android.app.NotificationManager.class);
|
||||
|
||||
if (notificationManager != null) {
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the notification
|
||||
builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
||||
// .setSmallIcon(R.drawable.ic_download) // 通知图标
|
||||
.setOnlyAlertOnce(true)
|
||||
.setAutoCancel(true) // 默认不自动取消
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT); // 默认优先级
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新通知
|
||||
* @param state 下载状态
|
||||
* @param progress 下载进度
|
||||
*/
|
||||
public void updateNotification(String fileName, M3U8TaskState state, int progress) {
|
||||
if (builder == null) return;
|
||||
|
||||
builder.setContentTitle(fileName == null || fileName.equals("") ? "下载M3U8文件" : fileName);
|
||||
switch (state) {
|
||||
case PREPARE:
|
||||
notificationProgress = -100;
|
||||
builder.setContentText("准备下载").setProgress(0, 0, true);
|
||||
builder.setOngoing(true)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done);
|
||||
case PENDING:
|
||||
notificationProgress = -100;
|
||||
builder.setContentText("等待下载...").setProgress(0, 0, true);
|
||||
builder.setOngoing(true)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done);
|
||||
break;
|
||||
case DOWNLOADING:
|
||||
// 控制刷新Notification频率
|
||||
if (progress < 100 && (progress - notificationProgress < 2)) {
|
||||
return;
|
||||
}
|
||||
notificationProgress = progress;
|
||||
builder.setContentText("正在下载...")
|
||||
.setProgress(100, progress, false);
|
||||
builder.setOngoing(true)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download);
|
||||
break;
|
||||
case PAUSE:
|
||||
builder.setContentText("暂停下载");
|
||||
builder.setOngoing(false)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download);
|
||||
break;
|
||||
case SUCCESS:
|
||||
// 点击跳转
|
||||
Intent intent = new Intent(context, getMainActivityClass(context));
|
||||
intent.setAction(ACTION_SELECT_NOTIFICATION);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||
context, NOTIFICATION_ID,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
|
||||
);
|
||||
builder.setContentIntent(pendingIntent);
|
||||
|
||||
builder.setContentText("下载完成").setProgress(0, 0, false);
|
||||
builder.setOngoing(false)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done);
|
||||
break;
|
||||
case ERROR:
|
||||
case ENOSPC:
|
||||
builder.setContentText("下载失败").setProgress(0, 0, false);
|
||||
builder.setOngoing(false)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Show the notification
|
||||
NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, builder.build());
|
||||
}
|
||||
|
||||
public NotificationCompat.Builder getBuilder() {
|
||||
return builder;
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
if (notificationManager != null) {
|
||||
notificationManager.cancel(NOTIFICATION_ID);
|
||||
notificationManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
private Class getMainActivityClass(Context context) {
|
||||
String packageName = context.getPackageName();
|
||||
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
|
||||
String className = launchIntent.getComponent().getClassName();
|
||||
try {
|
||||
return Class.forName(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.vincent.m3u8Downloader.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @Author: Vincent
|
||||
* @CreateAt: 2021/08/25 17:16
|
||||
* @Desc: SharedPreferences帮助类
|
||||
*/
|
||||
public class SpHelper {
|
||||
|
||||
private static final String NULL_KEY = "NULL_KEY";
|
||||
private static final String TAG_NAME = "M3U8PreferenceHelper";
|
||||
|
||||
private static SharedPreferences PREFERENCES;
|
||||
|
||||
|
||||
public static void init(Context context) {
|
||||
PREFERENCES = context.getSharedPreferences(TAG_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
|
||||
private static String checkKeyNonNull(String key) {
|
||||
if (key == null) {
|
||||
Log.e(NULL_KEY, "Key is null!!!");
|
||||
return NULL_KEY;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
private static SharedPreferences.Editor newEditor() {
|
||||
return PREFERENCES.edit();
|
||||
}
|
||||
|
||||
public static void putBoolean(@NonNull String key, boolean value) {
|
||||
newEditor().putBoolean(checkKeyNonNull(key), value).apply();
|
||||
}
|
||||
|
||||
public static boolean getBoolean(@NonNull String key, boolean defValue) {
|
||||
return PREFERENCES.getBoolean(checkKeyNonNull(key), defValue);
|
||||
}
|
||||
|
||||
public static void putInt(@NonNull String key, int value) {
|
||||
newEditor().putInt(checkKeyNonNull(key), value).apply();
|
||||
}
|
||||
|
||||
public static int getInt(@NonNull String key, int defValue) {
|
||||
return PREFERENCES.getInt(checkKeyNonNull(key), defValue);
|
||||
}
|
||||
|
||||
public static void putString(@NonNull String key, @Nullable String value) {
|
||||
newEditor().putString(checkKeyNonNull(key), value).apply();
|
||||
}
|
||||
|
||||
public static String getString(@NonNull String key, @Nullable String defValue) {
|
||||
return PREFERENCES.getString(checkKeyNonNull(key), defValue);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user