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

@@ -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 插入项)不支持持久化权限,忽略即可
}
}
}