初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
@@ -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>
@@ -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});
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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()));
}
}
@@ -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,
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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());
}
};
}
@@ -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;
}
}
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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;
}
}
}
@@ -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);
}
}