feat(download): 支持分区存储Uri下载模式
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user