using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
using System.Threading.Tasks;
using System.Linq;
using System.Globalization;
///
/// 跨平台数据导出服务
/// 支持BFI测试记录、折线图和历史数据的导出
///
public class DataExportService : IDataExportService
{
private const string EXPORT_FOLDER = "DCX_Export";
private const string HISTORY_FOLDER = "History";
private const float ANDROID_USB_CACHE_SECONDS = 2f;
private string _lastExportedFilePath;
private string _lastErrorMessage;
private string _cachedAndroidUsbDirectory;
private bool _cachedAndroidUsbDirectoryWritable;
private float _cachedAndroidUsbDirectoryTime;
public string GetLastErrorMessage()
{
return _lastErrorMessage;
}
private void ClearLastError()
{
_lastErrorMessage = null;
}
private void SetLastError(string message)
{
_lastErrorMessage = message;
if (!string.IsNullOrEmpty(message))
{
Debug.LogWarning(message);
}
}
///
/// 导出数据到USB(跨平台实现),content为要写入的文件内容
///
public bool ExportToUsb(string suggestedFilename, string content = null)
{
try
{
ClearLastError();
#if UNITY_ANDROID && !UNITY_EDITOR
return ExportToAndroidStorage(suggestedFilename, content);
#else
return ExportToEditorPath(suggestedFilename, content);
#endif
}
catch (Exception ex)
{
SetLastError($"数据导出失败: {ex.Message}");
return false;
}
}
///
/// 导出多个历史记录
///
public bool ExportHistoryRecords(List records, ExportFormat format = ExportFormat.TXT)
{
try
{
ClearLastError();
if (records == null || records.Count == 0)
{
SetLastError("没有可导出的历史记录");
return false;
}
var emptyDetailRecords = records
.Where(record => record == null || record.DataPoints == null || record.DataPoints.Count == 0)
.ToList();
if (emptyDetailRecords.Count > 0)
{
string names = string.Join("、", emptyDetailRecords
.Select(GetRecordDisplayName)
.Take(3));
if (emptyDetailRecords.Count > 3)
{
names += "等";
}
SetLastError($"所选BFI记录缺少明细数据,无法导出:{names}");
return false;
}
if (records.Sum(record => GetValidDataPoints(record).Count) == 0)
{
SetLastError("所选BFI记录没有有效数据点,无法导出");
return false;
}
if (!CheckExportPermissions())
{
return false;
}
#if UNITY_ANDROID && !UNITY_EDITOR
if (HasAndroidSafUsbDirectoryPermission())
{
if (TryExportHistoryRecordsWithAndroidSaf(records, format))
{
return true;
}
if (!string.IsNullOrEmpty(_lastErrorMessage))
{
return false;
}
}
#endif
string filename = $"BFI_History_{format}_{DateTime.Now:yyyyMMdd_HHmmss_fff}";
string exportPath = GetExportPath(filename);
if (string.IsNullOrEmpty(exportPath))
{
SetLastError("未找到可写入的U盘导出目录");
return false;
}
switch (format)
{
case ExportFormat.TXT:
return ExportHistoryToTXT(records, exportPath);
case ExportFormat.CSV:
return ExportHistoryToCSV(records, exportPath);
case ExportFormat.PDF:
return ExportHistoryToPDF(records, exportPath);
default:
return ExportHistoryToTXT(records, exportPath);
}
}
catch (Exception ex)
{
SetLastError($"历史记录导出失败: {ex.Message}");
return false;
}
}
#if UNITY_ANDROID && !UNITY_EDITOR
private bool TryExportHistoryRecordsWithAndroidSaf(List records, ExportFormat format)
{
try
{
if (!HasAndroidSafUsbDirectoryPermission())
{
if (RequestAndroidSafUsbDirectoryPermission())
{
SetLastError("请在弹出的文件选择器中选择 U S B 根目录并授权,然后重新点击导出");
}
return false;
}
string fileName = $"BFI_History_{format}_{DateTime.Now:yyyyMMdd_HHmmss_fff}{GetExportFileExtension(format)}";
bool success;
if (format == ExportFormat.PDF)
{
byte[] pdfBytes = BuildSimplePdfBytes(BuildPdfReportLines(records));
success = AndroidSafWriteBytesFile(fileName, pdfBytes);
}
else
{
string content = BuildHistoryExportContent(records, format);
if (string.IsNullOrEmpty(content))
{
SetLastError("导出内容为空");
return false;
}
success = AndroidSafWriteTextFile(fileName, content);
}
if (success)
{
_lastExportedFilePath = $"U S B/DCX_Export/{fileName}";
Debug.Log($"历史记录已通过U盘目录授权导出: {_lastExportedFilePath}");
return true;
}
ClearAndroidSafUsbDirectoryPermission();
if (RequestAndroidSafUsbDirectoryPermission())
{
SetLastError("U盘目录授权已失效或当前U盘不可写,已重新打开授权窗口。请选择 U S B 根目录并授权后再次导出");
}
return false;
}
catch (Exception ex)
{
SetLastError($"U盘授权写入失败: {ex.Message}");
return false;
}
}
private string GetExportFileExtension(ExportFormat format)
{
switch (format)
{
case ExportFormat.CSV:
return "_history.csv";
case ExportFormat.PDF:
return "_history.pdf";
case ExportFormat.TXT:
default:
return "_history.txt";
}
}
private string BuildHistoryExportContent(List records, ExportFormat format)
{
switch (format)
{
case ExportFormat.CSV:
return BuildHistoryCsvContent(records);
case ExportFormat.TXT:
default:
return BuildHistoryTxtContent(records);
}
}
private string BuildHistoryTxtContent(List records)
{
var reportBuilder = new StringBuilder();
reportBuilder.AppendLine("BFI测试历史报告");
reportBuilder.AppendLine($"生成时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
reportBuilder.AppendLine($"记录数量: {records.Count}");
reportBuilder.AppendLine();
foreach (var record in records)
{
reportBuilder.AppendLine(GenerateBFIReportContent(record));
reportBuilder.AppendLine("" + new string('=', 50));
}
return reportBuilder.ToString();
}
private string BuildHistoryCsvContent(List records)
{
var csvBuilder = new StringBuilder();
csvBuilder.AppendLine("测试名称,患者姓名,住院号,性别,年龄,身高(cm),体重(kg),开始时间,结束时间,时间戳,BFI值,状态,备注");
foreach (var record in records)
{
DateTime startTime = GetEffectiveStartTime(record);
DateTime endTime = GetEffectiveEndTime(record, startTime);
var validPoints = GetValidDataPoints(record);
string patientName = EscapeCsv(GetPatientName(record));
string patientId = EscapeCsv(GetPatientId(record));
string patientGender = EscapeCsv(GetPatientGender(record));
string patientAge = FormatPatientAge(record.PatientAge);
string patientHeight = FormatPatientHeight(record.PatientHeight);
string patientWeight = FormatPatientWeight(record.PatientWeight);
if (validPoints.Count == 0)
{
csvBuilder.AppendLine($"{EscapeCsv(record.TestName)},{patientName},{patientId},{patientGender},{patientAge},{patientHeight},{patientWeight},{startTime:yyyy-MM-dd HH:mm:ss},{endTime:yyyy-MM-dd HH:mm:ss},,,无有效数据点,");
continue;
}
foreach (var dataPoint in validPoints)
{
csvBuilder.AppendLine($"{EscapeCsv(record.TestName)},{patientName},{patientId},{patientGender},{patientAge},{patientHeight},{patientWeight},{startTime:yyyy-MM-dd HH:mm:ss},{endTime:yyyy-MM-dd HH:mm:ss},{dataPoint.Timestamp:yyyy-MM-dd HH:mm:ss},{dataPoint.BFI:F2},{EscapeCsv(dataPoint.Status)},{EscapeCsv(dataPoint.Notes)}");
}
}
return csvBuilder.ToString();
}
private bool HasAndroidSafUsbDirectoryPermission()
{
using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (var currentActivity = unityClass.GetStatic("currentActivity"))
using (var usbAccess = new AndroidJavaClass("com.dcx.ruiyiweiux.UsbStorageAccess"))
{
return usbAccess.CallStatic("hasPersistedUsbDirectory", currentActivity);
}
}
private bool RequestAndroidSafUsbDirectoryPermission()
{
try
{
using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (var currentActivity = unityClass.GetStatic("currentActivity"))
using (var usbAccess = new AndroidJavaClass("com.dcx.ruiyiweiux.UsbStorageAccess"))
{
usbAccess.CallStatic("requestUsbDirectoryPermission", currentActivity);
return true;
}
}
catch (Exception ex)
{
SetLastError($"无法打开U盘目录授权窗口: {ex.Message}");
return false;
}
}
private void ClearAndroidSafUsbDirectoryPermission()
{
using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (var currentActivity = unityClass.GetStatic("currentActivity"))
using (var usbAccess = new AndroidJavaClass("com.dcx.ruiyiweiux.UsbStorageAccess"))
{
usbAccess.CallStatic("clearPersistedUsbDirectory", currentActivity);
}
}
private bool AndroidSafWriteTextFile(string fileName, string content)
{
using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (var currentActivity = unityClass.GetStatic("currentActivity"))
using (var usbAccess = new AndroidJavaClass("com.dcx.ruiyiweiux.UsbStorageAccess"))
{
return usbAccess.CallStatic("writeTextFile", currentActivity, fileName, content);
}
}
private bool AndroidSafWriteBytesFile(string fileName, byte[] bytes)
{
using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (var currentActivity = unityClass.GetStatic("currentActivity"))
using (var usbAccess = new AndroidJavaClass("com.dcx.ruiyiweiux.UsbStorageAccess"))
{
return usbAccess.CallStatic("writeBytesFile", currentActivity, fileName, bytes);
}
}
private int GetAndroidSdkInt()
{
using (var buildVersion = new AndroidJavaClass("android.os.Build$VERSION"))
{
return buildVersion.GetStatic("SDK_INT");
}
}
#endif
#region 平台特定实现
#if UNITY_ANDROID && !UNITY_EDITOR
///
/// Android平台导出到存储
/// 导出路径:/sdcard/Download/DCX_Export/
///
private bool ExportToAndroidStorage(string filename, string content = null)
{
try
{
if (GetAndroidSdkInt() >= 29)
{
if (HasAndroidSafUsbDirectoryPermission())
{
bool safSuccess = AndroidSafWriteTextFile(filename, content ?? string.Empty);
if (safSuccess)
{
_lastExportedFilePath = $"U S B/DCX_Export/{filename}";
Debug.Log($"Android: 文件已通过U盘目录授权写入 {_lastExportedFilePath}");
return true;
}
ClearAndroidSafUsbDirectoryPermission();
if (RequestAndroidSafUsbDirectoryPermission())
{
SetLastError("U盘目录授权已失效或当前U盘不可写,已重新打开授权窗口。请选择 U S B 根目录并授权后再次导出");
}
return false;
}
}
string exportDir = GetExportDirectory();
if (string.IsNullOrEmpty(exportDir))
{
if (GetAndroidSdkInt() >= 29 && RequestAndroidSafUsbDirectoryPermission())
{
SetLastError("未检测到可直接写入的U盘路径。请在弹出的文件选择器中选择 U S B 根目录并授权,然后重新点击导出");
}
else
{
SetLastError("未检测到可写入的U盘,请插入U盘后重试");
}
return false;
}
exportDir = EnsureExportDirectoryWithRootFolder(exportDir);
if (!Directory.Exists(exportDir))
{
Directory.CreateDirectory(exportDir);
}
string fullPath = Path.Combine(exportDir, filename);
// 写入文件内容
if (content != null)
{
File.WriteAllText(fullPath, content, Encoding.UTF8);
Debug.Log($"Android: 文件已写入 {fullPath}");
_lastExportedFilePath = fullPath;
}
else
{
Debug.Log($"Android: 导出目录已准备 {fullPath}(无内容写入)");
}
// 通知媒体扫描器更新文件,使其在文件管理器中可见
using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (var currentActivity = unityClass.GetStatic("currentActivity"))
{
NotifyMediaScanner(currentActivity, fullPath);
}
return true;
}
catch (Exception ex)
{
SetLastError($"Android导出失败: {ex.Message}");
return false;
}
}
///
/// 通知Android媒体扫描器更新文件
///
private void NotifyMediaScanner(AndroidJavaObject activity, string filePath)
{
try
{
var intent = new AndroidJavaObject("android.content.Intent");
intent.Call("setAction", "android.intent.action.MEDIA_SCANNER_SCAN_FILE");
var uri = new AndroidJavaClass("android.net.Uri");
var fileUri = uri.CallStatic("parse", $"file://{filePath}");
intent.Call("setData", fileUri);
activity.Call("sendBroadcast", intent);
}
catch (Exception ex)
{
Debug.LogWarning($"媒体扫描器通知失败: {ex.Message}");
}
}
#else
///
/// 编辑器/Windows模式导出
///
private bool ExportToEditorPath(string filename, string content = null)
{
try
{
string exportDir = EnsureExportDirectoryWithRootFolder(GetExportDirectory());
if (!Directory.Exists(exportDir))
{
Directory.CreateDirectory(exportDir);
}
string fullPath = Path.Combine(exportDir, filename);
// 写入文件内容
if (content != null)
{
File.WriteAllText(fullPath, content, Encoding.UTF8);
Debug.Log($"编辑器: 文件已写入 {fullPath}");
_lastExportedFilePath = fullPath;
}
else
{
Debug.Log($"编辑器: 导出目录已准备 {fullPath}(无内容写入)");
}
return true;
}
catch (Exception ex)
{
Debug.LogError($"编辑器导出失败: {ex.Message}");
return false;
}
}
///
/// 获取Windows平台USB设备驱动器列表
///
private List GetUsbDrives()
{
var usbDrives = new List();
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
try
{
foreach (var drive in System.IO.DriveInfo.GetDrives())
{
if (!drive.IsReady)
{
continue;
}
string root = drive.RootDirectory.FullName;
bool isRemovable = drive.DriveType == System.IO.DriveType.Removable;
bool hasDcxExportFolder = Directory.Exists(Path.Combine(root, EXPORT_FOLDER));
bool isSystemDrive = string.Equals(
Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.Windows)),
root,
StringComparison.OrdinalIgnoreCase);
if (isRemovable || (!isSystemDrive && hasDcxExportFolder))
{
usbDrives.Add(root);
}
}
}
catch (Exception ex)
{
Debug.LogWarning($"获取USB驱动器失败: {ex.Message}");
}
#endif
return usbDrives;
}
#endif
#endregion
#if UNITY_ANDROID && !UNITY_EDITOR
private string GetAndroidUsbExportDirectory(bool requireWritable)
{
if (Application.platform == RuntimePlatform.Android &&
Time.unscaledTime - _cachedAndroidUsbDirectoryTime <= ANDROID_USB_CACHE_SECONDS &&
!string.IsNullOrEmpty(_cachedAndroidUsbDirectory))
{
if (!requireWritable || _cachedAndroidUsbDirectoryWritable)
{
return _cachedAndroidUsbDirectory;
}
}
string firstCandidate = null;
foreach (var root in GetAndroidUsbCandidateRoots())
{
if (string.IsNullOrEmpty(root))
{
continue;
}
string exportDir = EnsureExportDirectoryWithRootFolder(root);
if (string.IsNullOrEmpty(firstCandidate))
{
firstCandidate = exportDir;
}
if (IsWritableDirectory(exportDir))
{
Debug.Log($"检测到可写U盘导出目录: {exportDir}");
_cachedAndroidUsbDirectory = exportDir;
_cachedAndroidUsbDirectoryWritable = true;
_cachedAndroidUsbDirectoryTime = Time.unscaledTime;
return exportDir;
}
}
_cachedAndroidUsbDirectory = firstCandidate;
_cachedAndroidUsbDirectoryWritable = false;
_cachedAndroidUsbDirectoryTime = Time.unscaledTime;
return requireWritable ? null : firstCandidate;
}
private IEnumerable GetAndroidUsbCandidateRoots()
{
var roots = new List();
AddAndroidUsbRootsFromStorageManager(roots);
AddAndroidUsbRootsFromEnvironment(roots);
AddAndroidUsbRoot(roots, "/storage/USB");
AddAndroidUsbRoot(roots, "/storage/U S B");
AddAndroidUsbRoot(roots, "/storage/usbotg");
AddAndroidUsbRoot(roots, "/storage/usb");
AddAndroidUsbRoot(roots, "/storage/udisk");
AddAndroidUsbRoot(roots, "/storage/udisk1");
AddAndroidUsbRoot(roots, "/mnt/usb");
AddAndroidUsbRoot(roots, "/mnt/udisk");
AddAndroidUsbRoot(roots, "/mnt/usb_storage/USB_DISK0");
AddAndroidUsbRoot(roots, "/mnt/usb_storage/USB_DISK1");
return roots;
}
private void AddAndroidUsbRootsFromStorageManager(List roots)
{
try
{
using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (var currentActivity = unityClass.GetStatic("currentActivity"))
using (var context = currentActivity.Call("getApplicationContext"))
{
var storageManager = context.Call("getSystemService", "storage");
if (storageManager == null)
{
return;
}
var volumes = storageManager.Call("getStorageVolumes");
if (volumes == null)
{
return;
}
int count = volumes.Call("size");
for (int i = 0; i < count; i++)
{
using (var volume = volumes.Call("get", i))
{
if (volume == null)
{
continue;
}
bool isPrimary = SafeCallBool(volume, "isPrimary");
bool isRemovable = SafeCallBool(volume, "isRemovable");
if (isPrimary || !isRemovable)
{
continue;
}
string description = TryGetStorageVolumeDescription(volume, context);
string path = TryGetStorageVolumeDirectory(volume);
if (!string.IsNullOrEmpty(path) &&
IsLikelyAndroidUsbRoot(description, path))
{
Debug.Log($"检测到可移动存储卷: {description}, 路径: {path}");
AddAndroidUsbRoot(roots, path);
}
}
}
}
}
catch (Exception ex)
{
Debug.LogWarning($"通过StorageManager扫描U盘失败: {ex.Message}");
}
}
private bool SafeCallBool(AndroidJavaObject target, string methodName)
{
try
{
return target != null && target.Call(methodName);
}
catch
{
return false;
}
}
private string TryGetStorageVolumeDescription(AndroidJavaObject volume, AndroidJavaObject context)
{
try
{
return volume.Call("getDescription", context);
}
catch
{
return null;
}
}
private string TryGetStorageVolumeDirectory(AndroidJavaObject volume)
{
try
{
using (var directory = volume.Call("getDirectory"))
{
string path = directory?.Call("getAbsolutePath");
if (!string.IsNullOrEmpty(path))
{
return path;
}
}
}
catch
{
// Android 10及以下没有公开getDirectory,继续尝试厂商/旧系统常见方法。
}
try
{
string path = volume.Call("getPath");
if (!string.IsNullOrEmpty(path))
{
return path;
}
}
catch
{
return null;
}
return null;
}
private void AddAndroidUsbRootsFromEnvironment(List roots)
{
AddAndroidUsbRootsFromEnvironmentValue(roots, Environment.GetEnvironmentVariable("SECONDARY_STORAGE"));
AddAndroidUsbRootsFromEnvironmentValue(roots, Environment.GetEnvironmentVariable("EXTERNAL_STORAGE"));
}
private void AddAndroidUsbRootsFromEnvironmentValue(List roots, string value)
{
if (string.IsNullOrEmpty(value))
{
return;
}
foreach (var path in value.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries))
{
AddAndroidUsbRoot(roots, path);
}
}
private bool IsLikelyAndroidUsbRoot(string name, string path)
{
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(path))
{
return false;
}
string lowerName = name.ToLowerInvariant();
string lowerPath = path.ToLowerInvariant();
string normalizedName = lowerName.Replace(" ", string.Empty)
.Replace("_", string.Empty)
.Replace("-", string.Empty);
if (lowerName == "emulated" || lowerName == "self" || lowerName == "sdcard0" || lowerName == "sdcard")
{
return false;
}
if (lowerPath.Contains("/android/data") || lowerPath.Contains("/obb"))
{
return false;
}
return lowerName.Contains("usb") ||
normalizedName.Contains("usb") ||
lowerName.Contains("udisk") ||
lowerName.Contains("otg") ||
lowerName.Contains("disk") ||
System.Text.RegularExpressions.Regex.IsMatch(name, "^[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}$");
}
private void AddAndroidUsbRoot(List roots, string root)
{
if (string.IsNullOrEmpty(root))
{
return;
}
if (IsAndroidInternalStorageRoot(root))
{
return;
}
try
{
string fullPath = Path.GetFullPath(root);
#if UNITY_ANDROID && !UNITY_EDITOR
if (!roots.Any(existing => string.Equals(existing, fullPath, StringComparison.OrdinalIgnoreCase)))
{
roots.Add(fullPath);
}
#else
if (Directory.Exists(fullPath) && !roots.Any(existing => string.Equals(existing, fullPath, StringComparison.OrdinalIgnoreCase)))
{
roots.Add(fullPath);
}
#endif
}
catch
{
#if UNITY_ANDROID && !UNITY_EDITOR
if (!roots.Contains(root))
{
roots.Add(root);
}
#else
if (Directory.Exists(root) && !roots.Contains(root))
{
roots.Add(root);
}
#endif
}
}
private bool IsAndroidInternalStorageRoot(string root)
{
if (string.IsNullOrEmpty(root))
{
return true;
}
string normalized = root.Replace('\\', '/').TrimEnd('/').ToLowerInvariant();
return normalized == "/sdcard" ||
normalized == "/storage/emulated" ||
normalized.StartsWith("/storage/emulated/", StringComparison.OrdinalIgnoreCase) ||
normalized == "/storage/self" ||
normalized.StartsWith("/storage/self/", StringComparison.OrdinalIgnoreCase);
}
#endif
#region BFI数据导出实现
///
/// 导出BFI折线图为图片
///
private bool ExportHistoryToImage(List records, string exportPath)
{
try
{
// 这里需要图表生成库
// 临时实现:生成数据点描述
foreach (var record in records)
{
DateTime startTime = GetEffectiveStartTime(record);
DateTime endTime = GetEffectiveEndTime(record, startTime);
var validPoints = GetValidDataPoints(record);
var imageDataBuilder = new StringBuilder();
imageDataBuilder.AppendLine($"BFI折线图数据 - {record.TestName}");
imageDataBuilder.AppendLine($"患者姓名: {GetPatientName(record)}");
imageDataBuilder.AppendLine($"住院号: {GetPatientId(record)}");
imageDataBuilder.AppendLine($"性别: {GetPatientGender(record)}");
imageDataBuilder.AppendLine($"年龄: {FormatPatientAge(record.PatientAge)}");
imageDataBuilder.AppendLine($"身高(cm): {FormatPatientHeight(record.PatientHeight)}");
imageDataBuilder.AppendLine($"体重(kg): {FormatPatientWeight(record.PatientWeight)}");
imageDataBuilder.AppendLine($"开始时间: {startTime:yyyy-MM-dd HH:mm:ss}");
imageDataBuilder.AppendLine($"结束时间: {endTime:yyyy-MM-dd HH:mm:ss}");
imageDataBuilder.AppendLine($"测试时长: {GetEffectiveDurationMinutes(startTime, endTime):F1} 分钟");
imageDataBuilder.AppendLine($"有效数据点: {validPoints.Count}/{record?.DataPoints?.Count ?? 0}");
imageDataBuilder.AppendLine("数据点:");
foreach (var point in validPoints)
{
imageDataBuilder.AppendLine($" {FormatPointTime(point.Timestamp)} - BFI: {point.BFI:F2}");
}
string txtPath = exportPath + "_chart.txt";
File.WriteAllText(txtPath, imageDataBuilder.ToString(), Encoding.UTF8);
Debug.Log($"BFI图表数据已导出: {txtPath}");
}
return true;
}
catch (Exception ex)
{
Debug.LogError($"图表导出失败: {ex.Message}");
return false;
}
}
#endregion
#region 历史记录导出
///
/// 导出历史记录为TXT
///
private bool ExportHistoryToTXT(List records, string exportPath)
{
try
{
var reportBuilder = new StringBuilder();
reportBuilder.AppendLine("BFI测试历史报告");
reportBuilder.AppendLine($"生成时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
reportBuilder.AppendLine($"记录数量: {records.Count}");
reportBuilder.AppendLine();
foreach (var record in records)
{
reportBuilder.AppendLine(GenerateBFIReportContent(record));
reportBuilder.AppendLine("" + new string('=', 50));
}
string txtPath = GetUniqueFilePath(exportPath + "_history.txt");
string content = reportBuilder.ToString();
if (!WriteTextFileVerified(txtPath, content))
{
return false;
}
_lastExportedFilePath = txtPath;
NotifyExportedFile(txtPath);
Debug.Log($"历史记录报告已生成: {txtPath}");
return true;
}
catch (Exception ex)
{
Debug.LogError($"历史记录TXT导出失败: {ex.Message}");
return false;
}
}
///
/// 导出历史记录为CSV
///
private bool ExportHistoryToCSV(List records, string exportPath)
{
try
{
var csvBuilder = new StringBuilder();
csvBuilder.AppendLine("测试名称,患者姓名,住院号,性别,年龄,身高(cm),体重(kg),开始时间,结束时间,时间戳,BFI值,状态,备注");
foreach (var record in records)
{
DateTime startTime = GetEffectiveStartTime(record);
DateTime endTime = GetEffectiveEndTime(record, startTime);
var validPoints = GetValidDataPoints(record);
string patientName = EscapeCsv(GetPatientName(record));
string patientId = EscapeCsv(GetPatientId(record));
string patientGender = EscapeCsv(GetPatientGender(record));
string patientAge = FormatPatientAge(record.PatientAge);
string patientHeight = FormatPatientHeight(record.PatientHeight);
string patientWeight = FormatPatientWeight(record.PatientWeight);
if (validPoints.Count == 0)
{
csvBuilder.AppendLine($"{EscapeCsv(record.TestName)},{patientName},{patientId},{patientGender},{patientAge},{patientHeight},{patientWeight},{startTime:yyyy-MM-dd HH:mm:ss},{endTime:yyyy-MM-dd HH:mm:ss},,,无有效数据点,");
continue;
}
foreach (var dataPoint in validPoints)
{
csvBuilder.AppendLine($"{EscapeCsv(record.TestName)},{patientName},{patientId},{patientGender},{patientAge},{patientHeight},{patientWeight},{startTime:yyyy-MM-dd HH:mm:ss},{endTime:yyyy-MM-dd HH:mm:ss},{dataPoint.Timestamp:yyyy-MM-dd HH:mm:ss},{dataPoint.BFI:F2},{EscapeCsv(dataPoint.Status)},{EscapeCsv(dataPoint.Notes)}");
}
}
string csvPath = GetUniqueFilePath(exportPath + "_history.csv");
if (!WriteTextFileVerified(csvPath, csvBuilder.ToString()))
{
return false;
}
_lastExportedFilePath = csvPath;
NotifyExportedFile(csvPath);
Debug.Log($"历史记录CSV已导出: {csvPath}");
return true;
}
catch (Exception ex)
{
Debug.LogError($"历史记录CSV导出失败: {ex.Message}");
return false;
}
}
///
/// 导出历史记录为PDF
///
private bool ExportHistoryToPDF(List records, string exportPath)
{
try
{
var lines = BuildPdfReportLines(records);
string pdfPath = GetUniqueFilePath(exportPath + "_history.pdf");
byte[] pdfBytes = BuildSimplePdfBytes(lines);
if (!WriteBytesFileVerified(pdfPath, pdfBytes))
{
return false;
}
_lastExportedFilePath = pdfPath;
NotifyExportedFile(pdfPath);
Debug.Log($"历史记录PDF已导出: {pdfPath}");
return true;
}
catch (Exception ex)
{
Debug.LogError($"历史记录PDF导出失败: {ex.Message}");
return false;
}
}
#endregion
#region 辅助方法
///
/// 生成BFI文件名
///
private string GenerateBFIFilename(BFITestRecord record)
{
return $"BFI_{record.TestName}_{record.StartTime:yyyyMMdd_HHmmss}";
}
///
/// 获取导出路径
///
private string GetExportPath(string filename)
{
string exportDirectory = EnsureExportDirectoryWithRootFolder(GetExportDirectory());
if (!Directory.Exists(exportDirectory))
{
Directory.CreateDirectory(exportDirectory);
}
return Path.Combine(exportDirectory, filename);
}
private string GetUniqueFilePath(string filePath)
{
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
{
return filePath;
}
string directory = Path.GetDirectoryName(filePath);
string fileName = Path.GetFileNameWithoutExtension(filePath);
string extension = Path.GetExtension(filePath);
for (int i = 1; i < 10000; i++)
{
string candidate = Path.Combine(directory, $"{fileName}_{i}{extension}");
if (!File.Exists(candidate))
{
return candidate;
}
}
return Path.Combine(directory, $"{fileName}_{Guid.NewGuid():N}{extension}");
}
///
/// 生成BFI报告内容
///
private string GenerateBFIReportContent(BFITestRecord record)
{
var builder = new StringBuilder();
DateTime startTime = GetEffectiveStartTime(record);
DateTime endTime = GetEffectiveEndTime(record, startTime);
var validPoints = GetValidDataPoints(record);
int totalPoints = record?.DataPoints?.Count ?? 0;
int ignoredPoints = totalPoints - validPoints.Count;
builder.AppendLine($"BFI测试报告 - {record.TestName}");
builder.AppendLine($"患者姓名: {GetPatientName(record)}");
builder.AppendLine($"住院号: {GetPatientId(record)}");
builder.AppendLine($"性别: {GetPatientGender(record)}");
builder.AppendLine($"年龄: {FormatPatientAge(record.PatientAge)}");
builder.AppendLine($"身高(cm): {FormatPatientHeight(record.PatientHeight)}");
builder.AppendLine($"体重(kg): {FormatPatientWeight(record.PatientWeight)}");
builder.AppendLine($"开始时间: {startTime:yyyy-MM-dd HH:mm:ss}");
builder.AppendLine($"结束时间: {endTime:yyyy-MM-dd HH:mm:ss}");
builder.AppendLine($"测试时长: {GetEffectiveDurationMinutes(startTime, endTime):F1} 分钟");
builder.AppendLine($"数据点数量: {totalPoints}(有效: {validPoints.Count})");
if (record.EndTime <= DateTime.MinValue || record.EndTime < startTime)
{
builder.AppendLine("说明: 结束时间未正常记录,已按最后有效数据点时间进行估算。");
}
if (ignoredPoints > 0)
{
builder.AppendLine($"说明: 已过滤 {ignoredPoints} 条异常BFI数据点(超范围或非数值)。");
}
builder.AppendLine();
if (validPoints.Count > 0)
{
builder.AppendLine("BFI数据统计:");
var bfiValues = validPoints.ConvertAll(dp => dp.BFI);
builder.AppendLine($" 最大值: {bfiValues.Max():F2}");
builder.AppendLine($" 最小值: {bfiValues.Min():F2}");
builder.AppendLine($" 平均值: {bfiValues.Average():F2}");
builder.AppendLine();
builder.AppendLine("详细数据:");
foreach (var point in validPoints)
{
builder.AppendLine($" {FormatPointTime(point.Timestamp)} | BFI: {point.BFI:F2} | 状态: {point.Status}");
}
}
else
{
builder.AppendLine("无有效BFI数据点");
}
return builder.ToString();
}
private bool IsValidBfiValue(float value)
{
return !float.IsNaN(value) && !float.IsInfinity(value) && value >= 0f && value <= 1000f;
}
private List GetValidDataPoints(BFITestRecord record)
{
var validPoints = new List();
if (record?.DataPoints == null)
{
return validPoints;
}
foreach (var point in record.DataPoints)
{
if (point == null || !IsValidBfiValue(point.BFI))
{
continue;
}
validPoints.Add(point);
}
return validPoints;
}
private string GetRecordDisplayName(BFITestRecord record)
{
if (!string.IsNullOrEmpty(record?.TestName))
{
return record.TestName;
}
if (!string.IsNullOrEmpty(record?.TestId))
{
return record.TestId;
}
return "未知记录";
}
private DateTime GetEffectiveStartTime(BFITestRecord record)
{
if (record.StartTime > DateTime.MinValue)
{
return record.StartTime;
}
if (record?.DataPoints != null)
{
foreach (var point in record.DataPoints)
{
if (point != null && point.Timestamp > DateTime.MinValue)
{
return point.Timestamp;
}
}
}
return DateTime.Now;
}
private DateTime GetEffectiveEndTime(BFITestRecord record, DateTime startTime)
{
if (record.EndTime > startTime)
{
return record.EndTime;
}
DateTime lastPointTime = DateTime.MinValue;
if (record?.DataPoints != null)
{
foreach (var point in record.DataPoints)
{
if (point != null && point.Timestamp > lastPointTime)
{
lastPointTime = point.Timestamp;
}
}
}
if (lastPointTime > startTime)
{
return lastPointTime;
}
return startTime;
}
private double GetEffectiveDurationMinutes(DateTime startTime, DateTime endTime)
{
double duration = (endTime - startTime).TotalMinutes;
return duration < 0 ? 0 : duration;
}
private string FormatPointTime(DateTime timestamp)
{
return timestamp > DateTime.MinValue ? timestamp.ToString("HH:mm:ss") : "--:--:--";
}
private string GetPatientName(BFITestRecord record)
{
return string.IsNullOrWhiteSpace(record?.PatientName) ? "未知患者" : record.PatientName;
}
private string GetPatientId(BFITestRecord record)
{
return string.IsNullOrWhiteSpace(record?.PatientId) ? "-" : record.PatientId;
}
private string GetPatientGender(BFITestRecord record)
{
return string.IsNullOrWhiteSpace(record?.PatientGender) ? "-" : record.PatientGender;
}
private string FormatPatientAge(int age)
{
return age > 0 ? age.ToString() : "-";
}
private string FormatPatientHeight(float height)
{
return height > 0f ? height.ToString("F1") : "-";
}
private string FormatPatientWeight(float weight)
{
return weight > 0f ? weight.ToString("F1") : "-";
}
private string EscapeCsv(string value)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
string escaped = value.Replace("\"", "\"\"");
if (escaped.Contains(",") || escaped.Contains("\n") || escaped.Contains("\r") || escaped.Contains("\""))
{
return $"\"{escaped}\"";
}
return escaped;
}
private List BuildPdfReportLines(List records)
{
var lines = new List
{
"BFI测试历史报告",
$"生成时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}",
$"记录数量: {records.Count}",
string.Empty
};
foreach (var record in records)
{
DateTime startTime = GetEffectiveStartTime(record);
DateTime endTime = GetEffectiveEndTime(record, startTime);
var validPoints = GetValidDataPoints(record);
int totalPoints = record?.DataPoints?.Count ?? 0;
lines.Add($"测试名称: {record.TestName}");
lines.Add($"患者姓名: {GetPatientName(record)} 住院号: {GetPatientId(record)}");
lines.Add($"性别: {GetPatientGender(record)} 年龄: {FormatPatientAge(record.PatientAge)} 身高(cm): {FormatPatientHeight(record.PatientHeight)} 体重(kg): {FormatPatientWeight(record.PatientWeight)}");
lines.Add($"开始时间: {startTime:yyyy-MM-dd HH:mm:ss}");
lines.Add($"结束时间: {endTime:yyyy-MM-dd HH:mm:ss}");
lines.Add($"测试时长: {GetEffectiveDurationMinutes(startTime, endTime):F1} 分钟 数据点: {totalPoints}(有效: {validPoints.Count})");
if (validPoints.Count > 0)
{
var bfiValues = validPoints.ConvertAll(dp => dp.BFI);
lines.Add($"统计: 最大值 {bfiValues.Max():F2} 最小值 {bfiValues.Min():F2} 平均值 {bfiValues.Average():F2}");
lines.Add("详细数据(时间 / BFI / 状态):");
foreach (var point in validPoints.Take(120))
{
lines.Add($" {FormatPointTime(point.Timestamp)} {point.BFI:F2} {point.Status}");
}
if (validPoints.Count > 120)
{
lines.Add($" …… 其余 {validPoints.Count - 120} 个数据点请导出CSV查看。");
}
}
else
{
lines.Add("无有效BFI数据点");
}
lines.Add(new string('-', 48));
lines.Add(string.Empty);
}
return lines;
}
private void WriteSimplePdf(string filePath, List lines)
{
if (!WriteBytesFileVerified(filePath, BuildSimplePdfBytes(lines)))
{
throw new IOException(_lastErrorMessage ?? "PDF文件写入失败");
}
}
private bool WriteTextFileVerified(string filePath, string content)
{
byte[] bytes = Encoding.UTF8.GetBytes(content ?? string.Empty);
return WriteBytesFileVerified(filePath, bytes);
}
private bool WriteBytesFileVerified(string filePath, byte[] bytes)
{
try
{
if (string.IsNullOrEmpty(filePath))
{
SetLastError("导出路径为空");
return false;
}
bytes = bytes ?? new byte[0];
string directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read, 8192))
{
stream.Write(bytes, 0, bytes.Length);
FlushFileStream(stream);
}
var info = new FileInfo(filePath);
if (!info.Exists || info.Length < bytes.Length)
{
SetLastError($"导出文件写入不完整: {filePath}, 已写入 {info.Length} / {bytes.Length} 字节");
return false;
}
return true;
}
catch (Exception ex)
{
SetLastError($"写入导出文件失败: {filePath}, {ex.Message}");
return false;
}
}
private void FlushFileStream(FileStream stream)
{
try
{
stream.Flush(true);
}
catch
{
stream.Flush();
}
}
private byte[] BuildSimplePdfBytes(List lines)
{
const float pageWidth = 595f;
const float pageHeight = 842f;
const float marginLeft = 48f;
const float marginTop = 54f;
const float lineHeight = 17f;
const int maxCharsPerLine = 46;
var wrappedLines = new List();
foreach (var line in lines)
{
wrappedLines.AddRange(WrapTextForPdf(line, maxCharsPerLine));
}
int linesPerPage = Mathf.Max(1, Mathf.FloorToInt((pageHeight - marginTop - 48f) / lineHeight));
var pages = new List>();
for (int i = 0; i < wrappedLines.Count; i += linesPerPage)
{
pages.Add(wrappedLines.Skip(i).Take(linesPerPage).ToList());
}
if (pages.Count == 0)
{
pages.Add(new List { "无可导出内容" });
}
var objects = new List();
objects.Add(PdfAscii("<< /Type /Catalog /Pages 2 0 R >>"));
var kids = new StringBuilder();
for (int i = 0; i < pages.Count; i++)
{
int pageObjectNumber = 5 + i * 2;
kids.Append($"{pageObjectNumber} 0 R ");
}
objects.Add(PdfAscii($"<< /Type /Pages /Kids [{kids}] /Count {pages.Count} >>"));
objects.Add(PdfAscii("<< /Type /Font /Subtype /Type0 /BaseFont /STSong-Light /Encoding /UniGB-UCS2-H /DescendantFonts [4 0 R] >>"));
objects.Add(PdfAscii("<< /Type /Font /Subtype /CIDFontType0 /BaseFont /STSong-Light /CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 2 >> >>"));
for (int i = 0; i < pages.Count; i++)
{
int pageObjectNumber = 5 + i * 2;
int contentObjectNumber = pageObjectNumber + 1;
objects.Add(PdfAscii($"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {pageWidth.ToString(CultureInfo.InvariantCulture)} {pageHeight.ToString(CultureInfo.InvariantCulture)}] /Resources << /Font << /F1 3 0 R >> >> /Contents {contentObjectNumber} 0 R >>"));
byte[] content = BuildPdfPageContent(pages[i], pageHeight, marginLeft, marginTop, lineHeight);
objects.Add(PdfStream(content));
}
using (var stream = new MemoryStream())
{
WriteAscii(stream, "%PDF-1.4\n%\u00e2\u00e3\u00cf\u00d3\n");
var offsets = new List { 0 };
for (int i = 0; i < objects.Count; i++)
{
offsets.Add(stream.Position);
WriteAscii(stream, $"{i + 1} 0 obj\n");
stream.Write(objects[i], 0, objects[i].Length);
WriteAscii(stream, "\nendobj\n");
}
long xrefOffset = stream.Position;
WriteAscii(stream, $"xref\n0 {objects.Count + 1}\n");
WriteAscii(stream, "0000000000 65535 f \n");
for (int i = 1; i < offsets.Count; i++)
{
WriteAscii(stream, $"{offsets[i]:D10} 00000 n \n");
}
WriteAscii(stream, $"trailer\n<< /Size {objects.Count + 1} /Root 1 0 R >>\nstartxref\n{xrefOffset}\n%%EOF");
return stream.ToArray();
}
}
private IEnumerable WrapTextForPdf(string text, int maxChars)
{
if (string.IsNullOrEmpty(text))
{
yield return string.Empty;
yield break;
}
for (int index = 0; index < text.Length; index += maxChars)
{
int length = Math.Min(maxChars, text.Length - index);
yield return text.Substring(index, length);
}
}
private byte[] BuildPdfPageContent(List lines, float pageHeight, float marginLeft, float marginTop, float lineHeight)
{
var builder = new StringBuilder();
builder.AppendLine("BT");
builder.AppendLine("/F1 11 Tf");
builder.AppendLine($"{marginLeft.ToString(CultureInfo.InvariantCulture)} {(pageHeight - marginTop).ToString(CultureInfo.InvariantCulture)} Td");
bool firstLine = true;
foreach (var line in lines)
{
if (!firstLine)
{
builder.AppendLine($"0 -{lineHeight.ToString(CultureInfo.InvariantCulture)} Td");
}
builder.AppendLine($"{PdfUnicodeHexString(line)} Tj");
firstLine = false;
}
builder.AppendLine("ET");
return Encoding.ASCII.GetBytes(builder.ToString());
}
private string PdfUnicodeHexString(string value)
{
byte[] bytes = Encoding.BigEndianUnicode.GetBytes(value ?? string.Empty);
var builder = new StringBuilder(bytes.Length * 2 + 2);
builder.Append('<');
foreach (byte b in bytes)
{
builder.Append(b.ToString("X2"));
}
builder.Append('>');
return builder.ToString();
}
private byte[] PdfStream(byte[] content)
{
using (var memory = new MemoryStream())
{
WriteAscii(memory, $"<< /Length {content.Length} >>\nstream\n");
memory.Write(content, 0, content.Length);
WriteAscii(memory, "\nendstream");
return memory.ToArray();
}
}
private byte[] PdfAscii(string value)
{
return Encoding.ASCII.GetBytes(value);
}
private void WriteAscii(Stream stream, string value)
{
byte[] bytes = Encoding.ASCII.GetBytes(value);
stream.Write(bytes, 0, bytes.Length);
}
///
/// 规范化导出目录,保证最终目录以 DCX_Export 结尾
///
private string EnsureExportDirectoryWithRootFolder(string baseDirectory)
{
if (string.IsNullOrEmpty(baseDirectory))
{
return Path.Combine(Application.persistentDataPath, EXPORT_FOLDER);
}
string normalizedPath = baseDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
string folderName = Path.GetFileName(normalizedPath);
if (string.Equals(folderName, EXPORT_FOLDER, StringComparison.OrdinalIgnoreCase))
{
return normalizedPath;
}
return Path.Combine(normalizedPath, EXPORT_FOLDER);
}
private bool IsWritableDirectory(string directory)
{
try
{
if (string.IsNullOrEmpty(directory))
{
return false;
}
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
string testFile = Path.Combine(directory, ".dcx_write_test");
File.WriteAllText(testFile, DateTime.Now.ToString("O"), Encoding.UTF8);
File.Delete(testFile);
#if !UNITY_ANDROID || UNITY_EDITOR
if (!HasEnoughFreeSpace(directory, 10L * 1024L * 1024L))
{
return false;
}
#endif
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"目录不可写: {directory}, {ex.Message}");
return false;
}
}
private bool HasEnoughFreeSpace(string directory, long requiredBytes)
{
try
{
string root = Path.GetPathRoot(Path.GetFullPath(directory));
if (string.IsNullOrEmpty(root))
{
return true;
}
var drive = new DriveInfo(root);
if (!drive.IsReady)
{
return true;
}
if (drive.AvailableFreeSpace < requiredBytes)
{
Debug.LogWarning($"导出目录空间不足: {directory}, 剩余 {drive.AvailableFreeSpace / 1024 / 1024}MB");
return false;
}
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"检查导出目录剩余空间失败: {directory}, {ex.Message}");
return true;
}
}
private void NotifyExportedFile(string filePath)
{
#if UNITY_ANDROID && !UNITY_EDITOR
try
{
using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (var currentActivity = unityClass.GetStatic("currentActivity"))
{
NotifyMediaScanner(currentActivity, filePath);
}
}
catch (Exception ex)
{
Debug.LogWarning($"导出文件媒体扫描通知失败: {ex.Message}");
}
#endif
}
#endregion
#region IDataExportService接口实现
///
/// 检查是否可以导出
///
public bool CanExport()
{
#if UNITY_ANDROID && !UNITY_EDITOR
int sdkInt = GetAndroidSdkInt();
if (sdkInt >= 29)
{
if (HasAndroidSafUsbDirectoryPermission())
{
return true;
}
return !string.IsNullOrEmpty(GetAndroidUsbExportDirectory(requireWritable: true));
}
if (!HasAndroidUsbWritePermission(sdkInt))
{
return false;
}
return !string.IsNullOrEmpty(GetAndroidUsbExportDirectory(requireWritable: true));
#elif UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
string exportDir = EnsureExportDirectoryWithRootFolder(GetExportDirectory());
return IsWritableDirectory(exportDir);
#else
return true; // 编辑器模式总是可以导出
#endif
}
///
/// 获取导出目录
///
public string GetExportDirectory()
{
#if UNITY_ANDROID && !UNITY_EDITOR
if (GetAndroidSdkInt() >= 29)
{
string directUsbDirectory = GetAndroidUsbExportDirectory(requireWritable: true);
if (!string.IsNullOrEmpty(directUsbDirectory))
{
return directUsbDirectory;
}
return HasAndroidSafUsbDirectoryPermission() ? "U S B/DCX_Export" : null;
}
return GetAndroidUsbExportDirectory(requireWritable: true);
#elif UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
var usbDrives = GetUsbDrives();
if (usbDrives.Count > 0)
{
return Path.Combine(usbDrives[0], EXPORT_FOLDER);
}
else
{
string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
return Path.Combine(desktopPath, EXPORT_FOLDER);
}
#else
return Path.Combine(Application.dataPath, "..", "Exports");
#endif
}
public string GetLastExportedFilePath()
{
return _lastExportedFilePath;
}
///
/// 检查导出权限
///
public bool CheckExportPermissions()
{
try
{
#if UNITY_ANDROID && !UNITY_EDITOR
int sdkInt = GetAndroidSdkInt();
if (sdkInt >= 29)
{
if (HasAndroidSafUsbDirectoryPermission())
{
return true;
}
string androidScopedWritableUsbDirectory = GetAndroidUsbExportDirectory(requireWritable: true);
if (!string.IsNullOrEmpty(androidScopedWritableUsbDirectory))
{
return true;
}
string androidScopedDetectedUsbDirectory = GetAndroidUsbExportDirectory(requireWritable: false);
if (!string.IsNullOrEmpty(androidScopedDetectedUsbDirectory))
{
if (RequestAndroidSafUsbDirectoryPermission())
{
SetLastError($"检测到U盘路径 {androidScopedDetectedUsbDirectory},但系统不允许直接写入。请在弹出的文件选择器中选择 U S B 根目录并授权,然后重新点击导出");
}
}
else if (RequestAndroidSafUsbDirectoryPermission())
{
SetLastError("未检测到可直接写入的U盘路径。请在弹出的文件选择器中选择 U S B 根目录并授权,然后重新点击导出");
}
return false;
}
if (!HasAndroidUsbWritePermission(sdkInt))
{
RequestAndroidUsbPermission(sdkInt);
return false;
}
string writableUsbDirectory = GetAndroidUsbExportDirectory(requireWritable: true);
if (!string.IsNullOrEmpty(writableUsbDirectory))
{
return true;
}
string detectedUsbDirectory = GetAndroidUsbExportDirectory(requireWritable: false);
if (!string.IsNullOrEmpty(detectedUsbDirectory))
{
SetLastError($"检测到U盘但无法写入: {detectedUsbDirectory}。请检查U盘格式、只读状态或系统存储权限");
}
else
{
SetLastError("未检测到可写入的U盘,请确认U盘已插入并重新导出");
}
return false;
#elif UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
string exportDir = GetExportDirectory();
string finalExportDir = EnsureExportDirectoryWithRootFolder(exportDir);
if (!IsWritableDirectory(finalExportDir))
{
Debug.LogWarning($"导出目录不可访问: {finalExportDir}");
return false;
}
return true;
#endif
}
catch (Exception ex)
{
Debug.LogError($"权限检查失败: {ex.Message}");
return false;
}
}
private bool HasAndroidUsbWritePermission()
{
#if UNITY_ANDROID && !UNITY_EDITOR
return HasAndroidUsbWritePermission(new AndroidJavaClass("android.os.Build$VERSION").GetStatic("SDK_INT"));
#else
return true;
#endif
}
private bool HasAndroidUsbWritePermission(int sdkInt)
{
if (sdkInt <= 28)
{
return AndroidExportPermissionManager.HasWriteExternalStoragePermission();
}
return AndroidExportPermissionManager.HasManageExternalStoragePermission();
}
private void RequestAndroidUsbPermission(int sdkInt)
{
if (sdkInt <= 28)
{
SetLastError("缺少存储权限,已请求授权,请允许后重新导出");
AndroidExportPermissionManager.RequestWriteExternalStoragePermission();
}
else
{
SetLastError("缺少所有文件访问权限,已打开权限设置,请授权后重新导出");
AndroidExportPermissionManager.RequestManageExternalStoragePermission();
}
}
#endregion
}
///
/// 导出格式枚举
///
public enum ExportFormat
{
TXT, // TXT报告
CSV, // CSV数据
PDF, // PDF报告
}