diff --git a/sdk-core/src/main/java/com/xuqm/sdk/file/FileSDK.kt b/sdk-core/src/main/java/com/xuqm/sdk/file/FileSDK.kt index c81d2cb..4ea0801 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/file/FileSDK.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/file/FileSDK.kt @@ -254,30 +254,68 @@ object FileSDK { android.util.Log.d("XWV", "saveBlobDownload: fileName=$fileName, dest=$destination, b64Len=${base64Data.length}") val bytes = android.util.Base64.decode(base64Data, android.util.Base64.DEFAULT) android.util.Log.d("XWV", "saveBlobDownload: decoded ${bytes.size} bytes") - val baseDir = when (destination) { - FileDownloadDestination.PublicDownloads -> { - val appName = context.applicationInfo.loadLabel(context.packageManager).toString() - File( - android.os.Environment.getExternalStoragePublicDirectory( - android.os.Environment.DIRECTORY_DOWNLOADS - ), - appName, - ).apply { - val created = mkdirs() - android.util.Log.d("XWV", "saveBlobDownload: PublicDownloads dir=$absolutePath, created=$created, exists=${exists()}") - } - } - FileDownloadDestination.Sandbox -> { - (context.getExternalFilesDir(null) ?: context.filesDir).also { - android.util.Log.d("XWV", "saveBlobDownload: Sandbox dir=${it.absolutePath}") - } - } - } - val target = uniqueFile(baseDir, fileName.takeIf { it.isNotBlank() } ?: "download.bin") + + val safeName = fileName.takeIf { it.isNotBlank() } ?: "download.bin" + + // 尝试目标目录,如果 PublicDownloads 失败则回退到 Sandbox + val baseDir = resolveBaseDir(context, destination) + val target = uniqueFile(baseDir, safeName) android.util.Log.d("XWV", "saveBlobDownload: writing to ${target.absolutePath}") - target.writeBytes(bytes) - android.util.Log.d("XWV", "saveBlobDownload: done, size=${target.length()}") - return target + return try { + target.writeBytes(bytes) + android.util.Log.d("XWV", "saveBlobDownload: done, size=${target.length()}") + target + } catch (e: java.io.IOException) { + android.util.Log.w("XWV", "saveBlobDownload: primary dir failed (${e.message}), falling back to Sandbox") + val fallbackDir = resolveBaseDir(context, FileDownloadDestination.Sandbox) + val fallbackTarget = uniqueFile(fallbackDir, safeName) + fallbackTarget.writeBytes(bytes) + android.util.Log.d("XWV", "saveBlobDownload: fallback done, ${fallbackTarget.absolutePath}, size=${fallbackTarget.length()}") + fallbackTarget + } + } + + private fun resolveBaseDir(context: Context, destination: FileDownloadDestination): File = when (destination) { + FileDownloadDestination.PublicDownloads -> { + val appName = context.applicationInfo.loadLabel(context.packageManager).toString() + File( + android.os.Environment.getExternalStoragePublicDirectory( + android.os.Environment.DIRECTORY_DOWNLOADS + ), + appName, + ).apply { mkdirs() } + } + FileDownloadDestination.Sandbox -> { + context.getExternalFilesDir(null) ?: context.filesDir + } + } + + /** + * 检查是否拥有"所有文件访问权限"(MANAGE_EXTERNAL_STORAGE)。 + * Android 11+ 需要此权限才能写入公共 Downloads 目录。 + * Android 10 及以下始终返回 true(使用 WRITE_EXTERNAL_STORAGE)。 + */ + fun isManageStorageGranted(context: Context): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Environment.isExternalStorageManager() + } else { + true + } + } + + /** + * 返回跳转到"所有文件访问权限"设置页的 Intent。 + * 调用方应使用 startActivityForResult 或 registerForActivityResult 处理返回。 + */ + fun requestManageStorageIntent(context: Context): Intent? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return null + return try { + Intent(android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply { + data = Uri.parse("package:${context.packageName}") + } + } catch (_: Exception) { + Intent(android.provider.Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) + } } /**