feat(download): 支持分区存储Uri下载模式

This commit is contained in:
2026-07-27 01:38:57 +08:00
parent cb50a2dcd2
commit cef92d6a2f
25 changed files with 1037 additions and 54 deletions

View File

@@ -17,17 +17,26 @@
package com.arialyy.simple.core.download;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.webkit.MimeTypeMap;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;
@@ -48,6 +57,8 @@ import com.arialyy.simple.databinding.ActivitySingleBinding;
import com.arialyy.simple.widget.ProgressLayout;
import java.io.File;
import java.io.InputStream;
import java.security.MessageDigest;
import java.util.List;
import java.util.Map;
@@ -265,7 +276,15 @@ public class SingleTaskActivity extends BaseActivity<ActivitySingleBinding> {
Toast.makeText(SingleTaskActivity.this, getString(R.string.download_success),
Toast.LENGTH_SHORT).show();
ALog.d(TAG, "文件md5: e088677570afe2e9f847cc8159b932dd");
ALog.d(TAG, "下载完成的文件md5: " + CommonUtil.getFileMD5(new File(task.getFilePath())));
String fileUri = task.getDownloadEntity().getFileUri();
String md5;
if (!TextUtils.isEmpty(fileUri)) {
// Uri 下载场景(分区存储):通过 ContentResolver 读流计算 MD5
md5 = getUriFileMD5(Uri.parse(fileUri));
} else {
md5 = CommonUtil.getFileMD5(new File(task.getFilePath()));
}
ALog.d(TAG, "下载完成的文件md5: " + md5);
getBinding().pl.setInfo(task.getEntity());
getBinding().pl.setProgress(100);
}
@@ -276,19 +295,212 @@ public class SingleTaskActivity extends BaseActivity<ActivitySingleBinding> {
return R.layout.activity_single;
}
private static final int REQUEST_CODE_CREATE_FILE = 0x1001;
private void startD() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10+ 分区存储:让用户选择保存方式
new AlertDialog.Builder(this)
.setTitle("选择保存方式")
.setItems(new String[]{
"自动保存到 Download 目录(MediaStore)",
"自选保存位置(SAF 文件选择器)"
}, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == 0) {
startDownloadByMediaStore();
} else {
openSafFilePicker();
}
}
})
.show();
} else {
// Android 9 及以下:直接使用文件路径(需 WRITE_EXTERNAL_STORAGE 权限)
startDownloadByPath();
}
}
/**
* Android 10+ 通过 MediaStore 在公共 Download 目录创建条目并下载(无需用户交互)
*/
private void startDownloadByMediaStore() {
Uri uri = createDownloadUri(new File(mFilePath).getName());
if (uri == null) {
T.showShort(this, "创建下载文件失败");
return;
}
startDownloadByUri(uri);
}
/**
* Android 10+ 通过 SAF(ACTION_CREATE_DOCUMENT)让用户自选保存位置
*/
private void openSafFilePicker() {
String fileName = new File(mFilePath).getName();
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
String mimeType = getMimeType(fileName);
intent.setType(TextUtils.isEmpty(mimeType) ? "application/octet-stream" : mimeType);
intent.putExtra(Intent.EXTRA_TITLE, fileName);
try {
startActivityForResult(intent, REQUEST_CODE_CREATE_FILE);
} catch (Exception e) {
ALog.e(TAG, "打开系统文件选择器失败:" + e.getMessage());
T.showShort(this, "无法打开系统文件选择器");
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_CREATE_FILE && resultCode == RESULT_OK
&& data != null && data.getData() != null) {
Uri uri = data.getData();
// 持久化 Uri 权限,保证 App 重启后仍能续传/写入
try {
getContentResolver().takePersistableUriPermission(uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} catch (Exception e) {
ALog.w(TAG, "持久化 Uri 权限失败:" + e.getMessage());
}
startDownloadByUri(uri);
}
}
/**
* 使用 Uri 创建下载任务(分区存储场景)
*/
private void startDownloadByUri(Uri uri) {
mTaskId = Aria.download(SingleTaskActivity.this)
.load(mUrl)
.setFileUri(uri)
.option(createHttpOption())
.ignoreCheckPermissions()
.create();
}
/**
* 使用文件路径创建下载任务(Android 9 及以下)
*/
private void startDownloadByPath() {
mTaskId = Aria.download(SingleTaskActivity.this)
.load(mUrl)
.setFilePath(mFilePath, true)
.option(createHttpOption())
.ignoreCheckPermissions()
.create();
}
private HttpOption createHttpOption() {
HttpOption option = new HttpOption();
option.useServerFileName(true);
option.addHeader("1", "@")
.useServerFileName(true)
.setFileLenAdapter(new FileLenAdapter());
//option.setRequestType(RequestEnum.POST);
mTaskId = Aria.download(SingleTaskActivity.this)
.load(mUrl)
.setFilePath(mFilePath, true)
.option(option)
.ignoreCheckPermissions()
.create();
return option;
}
/**
* Android 10+ 通过 MediaStore 在公共 Download 目录创建下载条目
*
* @param fileName 文件名
* @return 可写的 Uri,失败返回 null
*/
private Uri createDownloadUri(String fileName) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return null;
}
try {
ContentValues values = new ContentValues();
values.put(MediaStore.Downloads.DISPLAY_NAME, fileName);
values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
String mimeType = getMimeType(fileName);
if (!TextUtils.isEmpty(mimeType)) {
values.put(MediaStore.Downloads.MIME_TYPE, mimeType);
}
// 先删除同名旧条目,避免系统自动重命名为 xxx(1).zip
Uri old = queryDownloadUri(fileName);
if (old != null) {
getContentResolver().delete(old, null, null);
}
return getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);
} catch (Exception e) {
ALog.e(TAG, "创建 MediaStore 下载条目失败:" + e.getMessage());
return null;
}
}
/**
* 查询公共 Download 目录下是否已存在同名条目(仅限本应用创建的条目)
*/
private Uri queryDownloadUri(String fileName) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return null;
}
android.database.Cursor cursor = getContentResolver().query(
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Downloads._ID},
MediaStore.Downloads.DISPLAY_NAME + "=?",
new String[]{fileName}, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
long id = cursor.getLong(0);
return Uri.withAppendedPath(MediaStore.Downloads.EXTERNAL_CONTENT_URI,
String.valueOf(id));
}
} finally {
cursor.close();
}
}
return null;
}
/**
* 通过 ContentResolver 读流计算 Uri 对应文件的 MD5(分区存储场景)
*
* @return MD5 字符串,失败返回 null
*/
private String getUriFileMD5(Uri uri) {
InputStream is = null;
try {
is = getContentResolver().openInputStream(uri);
if (is == null) {
return null;
}
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] buffer = new byte[8192];
int len;
while ((len = is.read(buffer)) != -1) {
digest.update(buffer, 0, len);
}
StringBuilder sb = new StringBuilder();
for (byte b : digest.digest()) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception e) {
ALog.e(TAG, "计算 Uri 文件 MD5 失败:" + e.getMessage());
return null;
} finally {
if (is != null) {
try {
is.close();
} catch (Exception ignored) {
}
}
}
}
private String getMimeType(String fileName) {
String ext = MimeTypeMap.getFileExtensionFromUrl(fileName);
if (TextUtils.isEmpty(ext)) {
return null;
}
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.toLowerCase());
}
@Override