DCS/ruiyiweiUX/Assets/Scripts/Services/DataExportService.cs

1818 lines
61 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
using System.Threading.Tasks;
using System.Linq;
using System.Globalization;
/// <summary>
/// 跨平台数据导出服务
/// 支持BFI测试记录、折线图和历史数据的导出
/// </summary>
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);
}
}
/// <summary>
/// 导出数据到USB跨平台实现content为要写入的文件内容
/// </summary>
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;
}
}
/// <summary>
/// 导出多个历史记录
/// </summary>
public bool ExportHistoryRecords(List<BFITestRecord> 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<BFITestRecord> 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<BFITestRecord> records, ExportFormat format)
{
switch (format)
{
case ExportFormat.CSV:
return BuildHistoryCsvContent(records);
case ExportFormat.TXT:
default:
return BuildHistoryTxtContent(records);
}
}
private string BuildHistoryTxtContent(List<BFITestRecord> 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<BFITestRecord> 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<AndroidJavaObject>("currentActivity"))
using (var usbAccess = new AndroidJavaClass("com.dcx.ruiyiweiux.UsbStorageAccess"))
{
return usbAccess.CallStatic<bool>("hasPersistedUsbDirectory", currentActivity);
}
}
private bool RequestAndroidSafUsbDirectoryPermission()
{
try
{
using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (var currentActivity = unityClass.GetStatic<AndroidJavaObject>("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<AndroidJavaObject>("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<AndroidJavaObject>("currentActivity"))
using (var usbAccess = new AndroidJavaClass("com.dcx.ruiyiweiux.UsbStorageAccess"))
{
return usbAccess.CallStatic<bool>("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<AndroidJavaObject>("currentActivity"))
using (var usbAccess = new AndroidJavaClass("com.dcx.ruiyiweiux.UsbStorageAccess"))
{
return usbAccess.CallStatic<bool>("writeBytesFile", currentActivity, fileName, bytes);
}
}
private int GetAndroidSdkInt()
{
using (var buildVersion = new AndroidJavaClass("android.os.Build$VERSION"))
{
return buildVersion.GetStatic<int>("SDK_INT");
}
}
#endif
#region
#if UNITY_ANDROID && !UNITY_EDITOR
/// <summary>
/// Android平台导出到存储
/// 导出路径:/sdcard/Download/DCX_Export/
/// </summary>
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<AndroidJavaObject>("currentActivity"))
{
NotifyMediaScanner(currentActivity, fullPath);
}
return true;
}
catch (Exception ex)
{
SetLastError($"Android导出失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 通知Android媒体扫描器更新文件
/// </summary>
private void NotifyMediaScanner(AndroidJavaObject activity, string filePath)
{
try
{
var intent = new AndroidJavaObject("android.content.Intent");
intent.Call<AndroidJavaObject>("setAction", "android.intent.action.MEDIA_SCANNER_SCAN_FILE");
var uri = new AndroidJavaClass("android.net.Uri");
var fileUri = uri.CallStatic<AndroidJavaObject>("parse", $"file://{filePath}");
intent.Call<AndroidJavaObject>("setData", fileUri);
activity.Call("sendBroadcast", intent);
}
catch (Exception ex)
{
Debug.LogWarning($"媒体扫描器通知失败: {ex.Message}");
}
}
#else
/// <summary>
/// 编辑器/Windows模式导出
/// </summary>
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;
}
}
/// <summary>
/// 获取Windows平台USB设备驱动器列表
/// </summary>
private List<string> GetUsbDrives()
{
var usbDrives = new List<string>();
#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<string> GetAndroidUsbCandidateRoots()
{
var roots = new List<string>();
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<string> roots)
{
try
{
using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (var currentActivity = unityClass.GetStatic<AndroidJavaObject>("currentActivity"))
using (var context = currentActivity.Call<AndroidJavaObject>("getApplicationContext"))
{
var storageManager = context.Call<AndroidJavaObject>("getSystemService", "storage");
if (storageManager == null)
{
return;
}
var volumes = storageManager.Call<AndroidJavaObject>("getStorageVolumes");
if (volumes == null)
{
return;
}
int count = volumes.Call<int>("size");
for (int i = 0; i < count; i++)
{
using (var volume = volumes.Call<AndroidJavaObject>("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<bool>(methodName);
}
catch
{
return false;
}
}
private string TryGetStorageVolumeDescription(AndroidJavaObject volume, AndroidJavaObject context)
{
try
{
return volume.Call<string>("getDescription", context);
}
catch
{
return null;
}
}
private string TryGetStorageVolumeDirectory(AndroidJavaObject volume)
{
try
{
using (var directory = volume.Call<AndroidJavaObject>("getDirectory"))
{
string path = directory?.Call<string>("getAbsolutePath");
if (!string.IsNullOrEmpty(path))
{
return path;
}
}
}
catch
{
// Android 10及以下没有公开getDirectory继续尝试厂商/旧系统常见方法。
}
try
{
string path = volume.Call<string>("getPath");
if (!string.IsNullOrEmpty(path))
{
return path;
}
}
catch
{
return null;
}
return null;
}
private void AddAndroidUsbRootsFromEnvironment(List<string> roots)
{
AddAndroidUsbRootsFromEnvironmentValue(roots, Environment.GetEnvironmentVariable("SECONDARY_STORAGE"));
AddAndroidUsbRootsFromEnvironmentValue(roots, Environment.GetEnvironmentVariable("EXTERNAL_STORAGE"));
}
private void AddAndroidUsbRootsFromEnvironmentValue(List<string> 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<string> 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数据导出实现
/// <summary>
/// 导出BFI折线图为图片
/// </summary>
private bool ExportHistoryToImage(List<BFITestRecord> 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
/// <summary>
/// 导出历史记录为TXT
/// </summary>
private bool ExportHistoryToTXT(List<BFITestRecord> 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;
}
}
/// <summary>
/// 导出历史记录为CSV
/// </summary>
private bool ExportHistoryToCSV(List<BFITestRecord> 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;
}
}
/// <summary>
/// 导出历史记录为PDF
/// </summary>
private bool ExportHistoryToPDF(List<BFITestRecord> 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
/// <summary>
/// 生成BFI文件名
/// </summary>
private string GenerateBFIFilename(BFITestRecord record)
{
return $"BFI_{record.TestName}_{record.StartTime:yyyyMMdd_HHmmss}";
}
/// <summary>
/// 获取导出路径
/// </summary>
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}");
}
/// <summary>
/// 生成BFI报告内容
/// </summary>
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<BFIDataPoint> GetValidDataPoints(BFITestRecord record)
{
var validPoints = new List<BFIDataPoint>();
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<string> BuildPdfReportLines(List<BFITestRecord> records)
{
var lines = new List<string>
{
"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<string> 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<string> 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<string>();
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<List<string>>();
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<string> { "无可导出内容" });
}
var objects = new List<byte[]>();
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<long> { 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<string> 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<string> 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);
}
/// <summary>
/// 规范化导出目录,保证最终目录以 DCX_Export 结尾
/// </summary>
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<AndroidJavaObject>("currentActivity"))
{
NotifyMediaScanner(currentActivity, filePath);
}
}
catch (Exception ex)
{
Debug.LogWarning($"导出文件媒体扫描通知失败: {ex.Message}");
}
#endif
}
#endregion
#region IDataExportService接口实现
/// <summary>
/// 检查是否可以导出
/// </summary>
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
}
/// <summary>
/// 获取导出目录
/// </summary>
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;
}
/// <summary>
/// 检查导出权限
/// </summary>
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<int>("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
}
/// <summary>
/// 导出格式枚举
/// </summary>
public enum ExportFormat
{
TXT, // TXT报告
CSV, // CSV数据
PDF, // PDF报告
}