feat(download): 支持分区存储Uri下载模式
This commit is contained in:
@@ -19,7 +19,7 @@ import com.arialyy.aria.core.wrapper.ITaskWrapper;
|
||||
import com.arialyy.aria.orm.DbEntity;
|
||||
import com.arialyy.aria.orm.annotation.Ignore;
|
||||
import com.arialyy.aria.orm.annotation.NoNull;
|
||||
import com.arialyy.aria.orm.annotation.Unique;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -78,6 +78,11 @@ public class TaskRecord extends DbEntity {
|
||||
*/
|
||||
public boolean isBlock = false;
|
||||
|
||||
/**
|
||||
* 是否是通过 Uri(分区存储场景)下载的任务 {@code true} 是
|
||||
*/
|
||||
public boolean isUriTask = false;
|
||||
|
||||
/**
|
||||
* 任务类型
|
||||
* {@link ITaskWrapper}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.arialyy.aria.core.common;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.arialyy.aria.core.TaskRecord;
|
||||
import com.arialyy.aria.core.ThreadRecord;
|
||||
import com.arialyy.aria.core.download.DownloadEntity;
|
||||
@@ -29,6 +31,7 @@ import com.arialyy.aria.util.ALog;
|
||||
import com.arialyy.aria.util.CommonUtil;
|
||||
import com.arialyy.aria.util.DbDataHelper;
|
||||
import com.arialyy.aria.util.FileUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
@@ -80,6 +83,7 @@ public abstract class RecordHandler implements IRecordHandler {
|
||||
mConfigFile = new File(CommonUtil.getFileConfigPath(false, mEntity.getFileName()));
|
||||
if (mConfigFile.exists()) {
|
||||
convertDb();
|
||||
applyUriMode();
|
||||
} else {
|
||||
onPre();
|
||||
mTaskRecord = DbDataHelper.getTaskRecord(getFilePath(), mEntity.getTaskType());
|
||||
@@ -91,12 +95,32 @@ public abstract class RecordHandler implements IRecordHandler {
|
||||
}
|
||||
initRecord(false);
|
||||
}
|
||||
// 必须在handlerTaskRecord之前处理Uri任务信息,否则RecordHelper中File操作会因filePath为null而崩溃
|
||||
applyUriMode();
|
||||
handlerTaskRecord(mTaskRecord);
|
||||
}
|
||||
saveRecord();
|
||||
return mTaskRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理分区存储(Uri)任务的记录信息
|
||||
*/
|
||||
private void applyUriMode() {
|
||||
if (mTaskRecord == null) {
|
||||
return;
|
||||
}
|
||||
mTaskRecord.isUriTask = isUriMode();
|
||||
if (mTaskRecord.isUriTask) {
|
||||
// 分区存储 Uri 任务使用单文件随机写,不使用分块
|
||||
mTaskRecord.isBlock = false;
|
||||
if (TextUtils.isEmpty(mTaskRecord.filePath)) {
|
||||
// 分区存储场景下实体没有文件路径,使用 Uri 字符串作为记录路径,避免后续File操作出现NPE
|
||||
mTaskRecord.filePath = getFilePath();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* convertDb 是兼容性代码 从3.4.1开始,线程配置信息将存储在数据库中。 将配置文件的内容复制到数据库中,并将配置文件删除
|
||||
*/
|
||||
@@ -198,12 +222,25 @@ public abstract class RecordHandler implements IRecordHandler {
|
||||
*/
|
||||
private String getFilePath() {
|
||||
if (mEntity instanceof DownloadEntity) {
|
||||
return ((DownloadEntity) mTaskWrapper.getEntity()).getFilePath();
|
||||
DownloadEntity de = (DownloadEntity) mTaskWrapper.getEntity();
|
||||
if (!TextUtils.isEmpty(de.getFileUri())) {
|
||||
// 分区存储场景下,使用 Uri 字符串作为路径标识
|
||||
return de.getFileUri();
|
||||
}
|
||||
return de.getFilePath();
|
||||
} else {
|
||||
return ((UploadEntity) mTaskWrapper.getEntity()).getFilePath();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为通过 Uri 下载的分区存储任务
|
||||
*/
|
||||
private boolean isUriMode() {
|
||||
return mEntity instanceof DownloadEntity
|
||||
&& !TextUtils.isEmpty(((DownloadEntity) mEntity).getFileUri());
|
||||
}
|
||||
|
||||
@Override public void accept(ILoaderVisitor visitor) {
|
||||
visitor.addComponent(this);
|
||||
}
|
||||
|
||||
@@ -15,14 +15,21 @@
|
||||
*/
|
||||
package com.arialyy.aria.core.common;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.arialyy.aria.core.AriaConfig;
|
||||
import com.arialyy.aria.core.TaskRecord;
|
||||
import com.arialyy.aria.core.ThreadRecord;
|
||||
import com.arialyy.aria.core.download.DownloadEntity;
|
||||
import com.arialyy.aria.core.loader.IRecordHandler;
|
||||
import com.arialyy.aria.core.wrapper.AbsTaskWrapper;
|
||||
import com.arialyy.aria.util.ALog;
|
||||
import com.arialyy.aria.util.BufferedRandomAccessFile;
|
||||
import com.arialyy.aria.util.CommonUtil;
|
||||
import com.arialyy.aria.util.FileUtil;
|
||||
import com.arialyy.aria.util.UriFileUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -47,6 +54,10 @@ public class RecordHelper {
|
||||
* 处理非分块的,多线程任务
|
||||
*/
|
||||
public void handleMultiRecord() {
|
||||
if (mTaskRecord.isUriTask) {
|
||||
handleUriMultiRecord();
|
||||
return;
|
||||
}
|
||||
// 默认线程分块长度
|
||||
long blockSize = mWrapper.getEntity().getFileSize() / mTaskRecord.threadRecords.size();
|
||||
File temp = new File(mTaskRecord.filePath);
|
||||
@@ -150,10 +161,81 @@ public class RecordHelper {
|
||||
//mWrapper.setNewTask(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分区存储任务的目标 Uri 字符串
|
||||
*/
|
||||
private String getUriString() {
|
||||
if (mWrapper.getEntity() instanceof DownloadEntity) {
|
||||
String uri = ((DownloadEntity) mWrapper.getEntity()).getFileUri();
|
||||
if (!TextUtils.isEmpty(uri)) {
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
return mTaskRecord.filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理分区存储(Uri)多线程任务的记录,Uri 任务不能使用{@link File}操作
|
||||
*/
|
||||
private void handleUriMultiRecord() {
|
||||
long fileSize = mWrapper.getEntity().getFileSize();
|
||||
long blockSize = fileSize / mTaskRecord.threadRecords.size();
|
||||
long len = UriFileUtil.getFileLength(AriaConfig.getInstance().getAPP(),
|
||||
Uri.parse(getUriString()));
|
||||
// 目标内容被清空或不存在,重新分配线程区间
|
||||
if (len <= 0) {
|
||||
ALog.w(TAG, String.format("Uri目标【%s】不存在或内容为空,重新分配线程区间", getUriString()));
|
||||
for (int i = 0; i < mTaskRecord.threadNum; i++) {
|
||||
long startL = i * blockSize, endL = (i + 1) * blockSize;
|
||||
ThreadRecord tr = mTaskRecord.threadRecords.get(i);
|
||||
tr.startLocation = startL;
|
||||
tr.isComplete = false;
|
||||
if (tr.threadId == (mTaskRecord.threadNum - 1)) {
|
||||
endL = fileSize;
|
||||
}
|
||||
tr.endLocation = endL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理分区存储(Uri)单线程任务的记录,Uri 任务不能使用{@link File}操作
|
||||
*/
|
||||
private void handleUriSingleThreadRecord() {
|
||||
ThreadRecord tr = mTaskRecord.threadRecords.get(0);
|
||||
long fileSize = mWrapper.getEntity().getFileSize();
|
||||
String uriStr = getUriString();
|
||||
long len = UriFileUtil.getFileLength(AriaConfig.getInstance().getAPP(), Uri.parse(uriStr));
|
||||
if (len < 0) {
|
||||
// 无法获取Uri文件长度(如无权限、流式内容),信任数据库记录继续下载
|
||||
if (!tr.isComplete && tr.endLocation <= 0) {
|
||||
tr.endLocation = fileSize;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (len > fileSize) {
|
||||
ALog.i(TAG, String.format("Uri文件【%s】长度错误,任务重新开始", uriStr));
|
||||
tr.startLocation = 0;
|
||||
tr.isComplete = false;
|
||||
tr.endLocation = fileSize;
|
||||
} else if (len != 0 && len == fileSize) {
|
||||
ALog.d(TAG, "文件长度一致,线程完成");
|
||||
tr.isComplete = true;
|
||||
} else if (len != tr.startLocation) {
|
||||
ALog.i(TAG, String.format("修正Uri任务【%s】的进度记录为:%s", uriStr, len));
|
||||
tr.startLocation = len;
|
||||
tr.isComplete = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单线程的任务的记录
|
||||
*/
|
||||
public void handleSingleThreadRecord() {
|
||||
if (mTaskRecord.isUriTask) {
|
||||
handleUriSingleThreadRecord();
|
||||
return;
|
||||
}
|
||||
// mTaskRecord.isBlock是为了兼容以前的文件格式
|
||||
File file = new File(
|
||||
mTaskRecord.isBlock ? String.format(IRecordHandler.SUB_PATH, mTaskRecord.filePath, 0)
|
||||
|
||||
@@ -15,11 +15,14 @@
|
||||
*/
|
||||
package com.arialyy.aria.core.common;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
|
||||
import com.arialyy.aria.core.AriaConfig;
|
||||
import com.arialyy.aria.core.ThreadRecord;
|
||||
import com.arialyy.aria.core.wrapper.AbsTaskWrapper;
|
||||
import com.arialyy.aria.core.wrapper.ITaskWrapper;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
@@ -39,6 +42,8 @@ public class SubThreadConfig {
|
||||
// 真正的下载地址,如果是30x,则是30x后的地址
|
||||
public String url;
|
||||
public File tempFile;
|
||||
// 分区存储场景下的下载目标 Uri,与 tempFile 互斥(二选一)
|
||||
public Uri tempUri;
|
||||
// 线程记录
|
||||
public ThreadRecord record;
|
||||
// 状态处理器
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package com.arialyy.aria.core.download;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.arialyy.aria.core.TaskOptionParams;
|
||||
import com.arialyy.aria.core.config.Configuration;
|
||||
import com.arialyy.aria.core.config.DownloadConfig;
|
||||
@@ -54,6 +57,11 @@ public class DTaskWrapper extends AbsTaskWrapper<DownloadEntity> {
|
||||
*/
|
||||
private String mTempFilePath;
|
||||
|
||||
/**
|
||||
* 分区存储场景下,下载目标的 Uri(字符串形式)临时变量,与 {@link #mTempFilePath} 互斥
|
||||
*/
|
||||
private String mTempFileUri;
|
||||
|
||||
public DTaskWrapper(DownloadEntity entity) {
|
||||
super(entity);
|
||||
}
|
||||
@@ -118,5 +126,19 @@ public class DTaskWrapper extends AbsTaskWrapper<DownloadEntity> {
|
||||
|
||||
public void setTempFilePath(String mTempFilePath) {
|
||||
this.mTempFilePath = mTempFilePath;
|
||||
this.mTempFileUri = null;
|
||||
}
|
||||
|
||||
public String getFileUri() {
|
||||
return mTempFileUri;
|
||||
}
|
||||
|
||||
public void setFileUri(Uri uri) {
|
||||
this.mTempFileUri = uri == null ? null : uri.toString();
|
||||
this.mTempFilePath = null;
|
||||
}
|
||||
|
||||
public boolean isUriMode() {
|
||||
return !TextUtils.isEmpty(mTempFileUri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package com.arialyy.aria.core.download;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.arialyy.aria.core.common.AbsNormalEntity;
|
||||
import com.arialyy.aria.core.wrapper.ITaskWrapper;
|
||||
import com.arialyy.aria.orm.DbEntity;
|
||||
@@ -34,6 +35,12 @@ import com.arialyy.aria.util.CommonUtil;
|
||||
public class DownloadEntity extends AbsNormalEntity implements Parcelable, Cloneable {
|
||||
@Unique private String downloadPath; //保存路径
|
||||
|
||||
/**
|
||||
* 分区存储(Scoped Storage)场景下,下载目标为 Uri(如 SAF 文档、MediaStore 条目)时,
|
||||
* 该字段持久化 Uri 的字符串形式,用于任务恢复时继续写入。
|
||||
*/
|
||||
private String fileUri;
|
||||
|
||||
/**
|
||||
* 所属任务组
|
||||
*/
|
||||
@@ -65,7 +72,10 @@ public class DownloadEntity extends AbsNormalEntity implements Parcelable, Clone
|
||||
*/
|
||||
public M3U8Entity getM3U8Entity() {
|
||||
if (TextUtils.isEmpty(downloadPath)) {
|
||||
ALog.e("DownloadEntity", "文件保存路径为空,获取m3u8实体之前需要设置文件保存路径");
|
||||
// 分区存储(Uri)下载任务没有文件路径,且不支持m3u8,静默返回null即可
|
||||
if (TextUtils.isEmpty(fileUri)) {
|
||||
ALog.e("DownloadEntity", "文件保存路径为空,获取m3u8实体之前需要设置文件保存路径");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (m3U8Entity == null) {
|
||||
@@ -151,6 +161,15 @@ public class DownloadEntity extends AbsNormalEntity implements Parcelable, Clone
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getFileUri() {
|
||||
return fileUri;
|
||||
}
|
||||
|
||||
public DownloadEntity setFileUri(String fileUri) {
|
||||
this.fileUri = fileUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override public DownloadEntity clone() throws CloneNotSupportedException {
|
||||
return (DownloadEntity) super.clone();
|
||||
}
|
||||
@@ -185,6 +204,7 @@ public class DownloadEntity extends AbsNormalEntity implements Parcelable, Clone
|
||||
@Override public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeString(this.downloadPath);
|
||||
dest.writeString(this.fileUri);
|
||||
dest.writeString(this.groupHash);
|
||||
dest.writeString(this.md5Code);
|
||||
dest.writeString(this.disposition);
|
||||
@@ -195,6 +215,7 @@ public class DownloadEntity extends AbsNormalEntity implements Parcelable, Clone
|
||||
protected DownloadEntity(Parcel in) {
|
||||
super(in);
|
||||
this.downloadPath = in.readString();
|
||||
this.fileUri = in.readString();
|
||||
this.groupHash = in.readString();
|
||||
this.md5Code = in.readString();
|
||||
this.disposition = in.readString();
|
||||
|
||||
@@ -15,27 +15,43 @@
|
||||
*/
|
||||
package com.arialyy.aria.core.loader;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.arialyy.aria.core.TaskRecord;
|
||||
import com.arialyy.aria.core.ThreadRecord;
|
||||
import com.arialyy.aria.core.common.AbsNormalEntity;
|
||||
import com.arialyy.aria.core.common.SubThreadConfig;
|
||||
import com.arialyy.aria.core.download.DTaskWrapper;
|
||||
import com.arialyy.aria.core.task.IThreadTaskAdapter;
|
||||
import com.arialyy.aria.core.wrapper.AbsTaskWrapper;
|
||||
import com.arialyy.aria.util.CommonUtil;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public abstract class AbsNormalTTBuilderAdapter {
|
||||
protected String TAG = CommonUtil.getClassName(this);
|
||||
protected AbsTaskWrapper wrapper;
|
||||
private File tempFile;
|
||||
private boolean mIsUriMode = false;
|
||||
private Uri mUri;
|
||||
|
||||
public AbsNormalTTBuilderAdapter() {
|
||||
}
|
||||
|
||||
protected void setWrapper(AbsTaskWrapper wrapper) {
|
||||
this.wrapper = wrapper;
|
||||
tempFile = new File(((AbsNormalEntity) wrapper.getEntity()).getFilePath());
|
||||
String uri = null;
|
||||
if (wrapper instanceof DTaskWrapper) {
|
||||
uri = ((DTaskWrapper) wrapper).getFileUri();
|
||||
}
|
||||
if (!TextUtils.isEmpty(uri)) {
|
||||
mIsUriMode = true;
|
||||
mUri = Uri.parse(uri);
|
||||
} else {
|
||||
tempFile = new File(((AbsNormalEntity) wrapper.getEntity()).getFilePath());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,11 +82,18 @@ public abstract class AbsNormalTTBuilderAdapter {
|
||||
boolean isBlock, int startNum) {
|
||||
SubThreadConfig config = new SubThreadConfig();
|
||||
config.url = getEntity().isRedirect() ? getEntity().getRedirectUrl() : getEntity().getUrl();
|
||||
config.tempFile =
|
||||
isBlock ? new File(
|
||||
String.format(IRecordHandler.SUB_PATH, tempFile.getPath(), threadRecord.threadId))
|
||||
: tempFile;
|
||||
config.isBlock = isBlock;
|
||||
if (mIsUriMode) {
|
||||
// 分区存储 Uri 模式:使用单文件多线程随机写,不使用分块子文件
|
||||
config.tempUri = mUri;
|
||||
config.tempFile = null;
|
||||
config.isBlock = false;
|
||||
} else {
|
||||
config.tempFile =
|
||||
isBlock ? new File(
|
||||
String.format(IRecordHandler.SUB_PATH, tempFile.getPath(), threadRecord.threadId))
|
||||
: tempFile;
|
||||
config.isBlock = isBlock;
|
||||
}
|
||||
config.startThreadNum = startNum;
|
||||
config.taskWrapper = wrapper;
|
||||
config.record = threadRecord;
|
||||
@@ -87,4 +110,11 @@ public abstract class AbsNormalTTBuilderAdapter {
|
||||
protected File getTempFile() {
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为分区存储(Uri)模式,该模式下{@link #getTempFile()}为null,不能进行File操作
|
||||
*/
|
||||
protected boolean isUriMode() {
|
||||
return mIsUriMode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ package com.arialyy.aria.core.loader;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.arialyy.aria.core.common.AbsEntity;
|
||||
import com.arialyy.aria.core.common.AbsNormalEntity;
|
||||
import com.arialyy.aria.core.common.CompleteInfo;
|
||||
@@ -25,14 +27,12 @@ import com.arialyy.aria.core.inf.IThreadStateManager;
|
||||
import com.arialyy.aria.core.listener.IDLoadListener;
|
||||
import com.arialyy.aria.core.listener.IEventListener;
|
||||
import com.arialyy.aria.core.manager.ThreadTaskManager;
|
||||
import com.arialyy.aria.core.task.AbsTask;
|
||||
import com.arialyy.aria.core.task.IThreadTask;
|
||||
import com.arialyy.aria.core.wrapper.AbsTaskWrapper;
|
||||
import com.arialyy.aria.exception.AriaException;
|
||||
import com.arialyy.aria.util.ALog;
|
||||
import com.arialyy.aria.util.FileUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 单文件
|
||||
@@ -44,7 +44,9 @@ public class NormalLoader<T extends AbsTaskWrapper> extends AbsNormalLoader<T> {
|
||||
|
||||
public NormalLoader(T wrapper, IEventListener listener) {
|
||||
super(wrapper, listener);
|
||||
mTempFile = new File(getEntity().getFilePath());
|
||||
// 分区存储 Uri 模式下,下载目标为 Uri 而非本地文件路径,getFilePath() 为空,不创建本地 File
|
||||
String filePath = getEntity().getFilePath();
|
||||
mTempFile = !TextUtils.isEmpty(filePath) ? new File(filePath) : null;
|
||||
EventMsgUtil.getDefault().register(this);
|
||||
setUpdateInterval(wrapper.getConfig().getUpdateInterval());
|
||||
}
|
||||
@@ -109,9 +111,13 @@ public class NormalLoader<T extends AbsTaskWrapper> extends AbsNormalLoader<T> {
|
||||
if (getListener() instanceof IDLoadListener) {
|
||||
((IDLoadListener) getListener()).onPostPre(getEntity().getFileSize());
|
||||
}
|
||||
File file = new File(getEntity().getFilePath());
|
||||
if (file.getParentFile() != null && !file.getParentFile().exists()) {
|
||||
FileUtil.createDir(file.getPath());
|
||||
String filePath = getEntity().getFilePath();
|
||||
// 分区存储 Uri 模式下没有本地文件路径,无需创建父目录
|
||||
if (!TextUtils.isEmpty(filePath)) {
|
||||
File file = new File(filePath);
|
||||
if (file.getParentFile() != null && !file.getParentFile().exists()) {
|
||||
FileUtil.createDir(file.getPath());
|
||||
}
|
||||
}
|
||||
// 处理记录、初始化状态管理器
|
||||
mRecord = mRecordHandler.getRecord(getFileSize());
|
||||
|
||||
@@ -19,6 +19,7 @@ import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import com.arialyy.aria.core.TaskRecord;
|
||||
import com.arialyy.aria.core.inf.IThreadStateManager;
|
||||
import com.arialyy.aria.core.listener.IEventListener;
|
||||
@@ -26,6 +27,7 @@ import com.arialyy.aria.exception.AriaException;
|
||||
import com.arialyy.aria.util.ALog;
|
||||
import com.arialyy.aria.util.CommonUtil;
|
||||
import com.arialyy.aria.util.FileUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -247,6 +249,10 @@ public class NormalThreadStateManager implements IThreadStateManager {
|
||||
* @return {@code true} 合并成功,{@code false}合并失败
|
||||
*/
|
||||
private boolean mergeFile() {
|
||||
if (mTaskRecord.isUriTask) {
|
||||
// 分区存储 Uri 任务数据已直接写入目标 Uri,无需合并
|
||||
return true;
|
||||
}
|
||||
if (mTaskRecord.threadNum == 1) {
|
||||
File targetFile = new File(mTaskRecord.filePath);
|
||||
if (targetFile.exists()){
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.Process;
|
||||
|
||||
import com.arialyy.aria.core.AriaConfig;
|
||||
import com.arialyy.aria.core.ThreadRecord;
|
||||
import com.arialyy.aria.core.common.AbsEntity;
|
||||
@@ -36,6 +37,8 @@ import com.arialyy.aria.util.CommonUtil;
|
||||
import com.arialyy.aria.util.ErrorHelp;
|
||||
import com.arialyy.aria.util.FileUtil;
|
||||
import com.arialyy.aria.util.NetUtils;
|
||||
import com.arialyy.aria.util.UriFileUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -94,6 +97,10 @@ public class ThreadTask implements IThreadTask, IThreadTaskObserver {
|
||||
}
|
||||
|
||||
private void checkFileExist() {
|
||||
if (getConfig().tempUri != null) {
|
||||
// 分区存储 Uri 模式:目标由 ContentResolver 管理,无需预创建文件
|
||||
return;
|
||||
}
|
||||
if (!getConfig().tempFile.exists()) {
|
||||
FileUtil.createFile(getConfig().tempFile);
|
||||
}
|
||||
@@ -111,6 +118,9 @@ public class ThreadTask implements IThreadTask, IThreadTaskObserver {
|
||||
* 当前线程处理的文件名
|
||||
*/
|
||||
protected String getFileName() {
|
||||
if (getConfig().tempUri != null) {
|
||||
return UriFileUtil.getFileName(AriaConfig.getInstance().getAPP(), getConfig().tempUri);
|
||||
}
|
||||
return mConfig.tempFile.getName();
|
||||
}
|
||||
|
||||
@@ -311,7 +321,8 @@ public class ThreadTask implements IThreadTask, IThreadTaskObserver {
|
||||
|
||||
@Override public synchronized void updateCompleteState() {
|
||||
ALog.i(TAG, String.format("任务【%s】线程__%s__完成, blockSize = %s", getTaskWrapper().getKey(),
|
||||
mRecord.threadId, mConfig.tempFile.length()));
|
||||
mRecord.threadId,
|
||||
getConfig().tempUri != null ? "uri" : mConfig.tempFile.length()));
|
||||
writeConfig(true, mRecord.endLocation);
|
||||
// 进度发送不是实时的,发送完成任务前,需要更新一次进度
|
||||
sendRunningState();
|
||||
|
||||
@@ -16,12 +16,14 @@
|
||||
package com.arialyy.aria.orm;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.arialyy.aria.core.TaskRecord;
|
||||
import com.arialyy.aria.core.ThreadRecord;
|
||||
import com.arialyy.aria.core.download.DownloadEntity;
|
||||
import com.arialyy.aria.core.download.DownloadGroupEntity;
|
||||
import com.arialyy.aria.core.download.M3U8Entity;
|
||||
import com.arialyy.aria.core.upload.UploadEntity;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -34,7 +36,7 @@ class DBConfig {
|
||||
static boolean DEBUG = false;
|
||||
static Map<String, Class<? extends DbEntity>> mapping = new LinkedHashMap<>();
|
||||
static String DB_NAME;
|
||||
static int VERSION = 58;
|
||||
static int VERSION = 59;
|
||||
|
||||
/**
|
||||
* 是否将数据库保存在Sd卡,{@code true} 是
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.arialyy.aria.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.TextUtils;
|
||||
|
||||
/**
|
||||
* 分区存储(Scoped Storage)相关 Uri 工具类,提供文件名解析、可写性探测和权限持久化能力。
|
||||
*/
|
||||
public class UriFileUtil {
|
||||
private static final String TAG = "UriFileUtil";
|
||||
|
||||
/**
|
||||
* 从 Uri 中获取文件名
|
||||
*
|
||||
* @param context 上下文,可为 null(仅用 lastPathSegment 兜底)
|
||||
* @param uri 文档 Uri
|
||||
* @return 文件名,无法获取时返回空串
|
||||
*/
|
||||
public static String getFileName(Context context, Uri uri) {
|
||||
if (uri == null) {
|
||||
return "";
|
||||
}
|
||||
String name = queryDisplayName(context, uri);
|
||||
if (!TextUtils.isEmpty(name)) {
|
||||
return name;
|
||||
}
|
||||
String last = uri.getLastPathSegment();
|
||||
if (!TextUtils.isEmpty(last)) {
|
||||
int idx = last.lastIndexOf('/');
|
||||
return idx >= 0 ? last.substring(idx + 1) : last;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String queryDisplayName(Context context, Uri uri) {
|
||||
if (!"content".equalsIgnoreCase(uri.getScheme()) || context == null) {
|
||||
return null;
|
||||
}
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = context.getContentResolver()
|
||||
.query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (idx >= 0) {
|
||||
return cursor.getString(idx);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ALog.w(TAG, "query display name error: " + e.getMessage());
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 Uri 是否可写(以读写方式打开)
|
||||
*/
|
||||
public static boolean canWrite(Context context, Uri uri) {
|
||||
if (context == null || uri == null) {
|
||||
return false;
|
||||
}
|
||||
ParcelFileDescriptor pfd = null;
|
||||
try {
|
||||
pfd = context.getContentResolver().openFileDescriptor(uri, "rw");
|
||||
return pfd != null;
|
||||
} catch (Exception e) {
|
||||
ALog.w(TAG, "uri canWrite test failed: " + e.getMessage());
|
||||
return false;
|
||||
} finally {
|
||||
if (pfd != null) {
|
||||
try {
|
||||
pfd.close();
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Uri 对应文件的长度
|
||||
*
|
||||
* @return 文件长度;无法获取(Uri 不存在、无权限、流式内容等)时返回 -1
|
||||
*/
|
||||
public static long getFileLength(Context context, Uri uri) {
|
||||
if (context == null || uri == null) {
|
||||
return -1;
|
||||
}
|
||||
ParcelFileDescriptor pfd = null;
|
||||
try {
|
||||
pfd = context.getContentResolver().openFileDescriptor(uri, "r");
|
||||
return pfd == null ? -1 : pfd.getStatSize();
|
||||
} catch (Exception e) {
|
||||
ALog.w(TAG, "get uri file length failed: " + e.getMessage());
|
||||
return -1;
|
||||
} finally {
|
||||
if (pfd != null) {
|
||||
try {
|
||||
pfd.close();
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试持久化 Uri 权限,以便应用重启后仍能访问(SAF 文档树/文件)。
|
||||
* 若 Uri 不支持持久化权限则静默忽略。
|
||||
*/
|
||||
public static void tryTakePersistablePermission(Context context, Uri uri) {
|
||||
if (context == null || uri == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
context.getContentResolver()
|
||||
.takePersistableUriPermission(uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
} catch (SecurityException | IllegalArgumentException ignored) {
|
||||
// 部分 Uri(如 MediaStore 插入项)不支持持久化权限,忽略即可
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.arialyy.aria.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import com.arialyy.aria.core.AriaConfig;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
/**
|
||||
* 基于 {@link android.content.ContentResolver} 的随机访问文件输出流。
|
||||
*
|
||||
* <p>用于分区存储(Scoped Storage)场景下通过 Uri(如 SAF 文档、MediaStore 条目)写入下载数据,
|
||||
* 支持断点续传(seek)。底层通过 {@link android.content.ContentResolver#openFileDescriptor(Uri, String)}
|
||||
* 以 "rw" 模式打开 {@link ParcelFileDescriptor},再使用 {@link FileChannel} 进行随机写。</p>
|
||||
*
|
||||
* <p>每个下载子线程各自打开一个独立的 {@link FileChannel},并通过 {@link #seek(long)} 定位到自己的
|
||||
* 区间写入,因此天然支持多线程断点续传。</p>
|
||||
*/
|
||||
public class UriRandomAccessFile implements Closeable {
|
||||
|
||||
private final Uri mUri;
|
||||
private ParcelFileDescriptor mPfd;
|
||||
private FileOutputStream mFos;
|
||||
private FileChannel mChannel;
|
||||
private long mCurrPos;
|
||||
|
||||
public UriRandomAccessFile(Uri uri, long startLocation) throws IOException {
|
||||
this(AriaConfig.getInstance().getAPP(), uri, startLocation);
|
||||
}
|
||||
|
||||
public UriRandomAccessFile(Context context, Uri uri, long startLocation) throws IOException {
|
||||
if (context == null) {
|
||||
throw new IOException("Context is null, cannot open uri: " + uri);
|
||||
}
|
||||
mUri = uri;
|
||||
mPfd = context.getContentResolver().openFileDescriptor(uri, "rw");
|
||||
if (mPfd == null) {
|
||||
throw new IOException("openFileDescriptor return null for uri: " + uri);
|
||||
}
|
||||
mFos = new FileOutputStream(mPfd.getFileDescriptor());
|
||||
mChannel = mFos.getChannel();
|
||||
if (startLocation > 0) {
|
||||
mChannel.position(startLocation);
|
||||
}
|
||||
mCurrPos = startLocation;
|
||||
}
|
||||
|
||||
public void seek(long pos) throws IOException {
|
||||
mChannel.position(pos);
|
||||
mCurrPos = pos;
|
||||
}
|
||||
|
||||
public long getFilePointer() {
|
||||
return mCurrPos;
|
||||
}
|
||||
|
||||
public void write(int b) throws IOException {
|
||||
ByteBuffer bb = ByteBuffer.wrap(new byte[]{(byte) b});
|
||||
while (bb.hasRemaining()) {
|
||||
mChannel.write(bb);
|
||||
}
|
||||
mCurrPos++;
|
||||
}
|
||||
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
ByteBuffer bb = ByteBuffer.wrap(b, off, len);
|
||||
while (bb.hasRemaining()) {
|
||||
mChannel.write(bb);
|
||||
}
|
||||
mCurrPos += len;
|
||||
}
|
||||
|
||||
public long length() throws IOException {
|
||||
return mChannel.size();
|
||||
}
|
||||
|
||||
public void flush() throws IOException {
|
||||
mFos.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
if (mChannel != null) {
|
||||
mChannel.close();
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (mFos != null) {
|
||||
mFos.close();
|
||||
}
|
||||
} finally {
|
||||
if (mPfd != null) {
|
||||
mPfd.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user