package com.safeluck.floatwindow.util; import android.content.Context; import android.os.StatFs; import android.os.storage.StorageManager; import java.io.File; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Locale; import timber.log.Timber; public class FileUtil { //获取插入的TFCard目录路径 public static String getStoragePath(Context mContext, boolean is_removale) { if (mContext != null) { StorageManager mStorageManager = (StorageManager) mContext.getSystemService(Context.STORAGE_SERVICE); Class storageVolumeClazz = null; try { storageVolumeClazz = Class.forName("android.os.storage.StorageVolume"); Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList"); Method getPath = storageVolumeClazz.getMethod("getPath"); Method isRemovable = storageVolumeClazz.getMethod("isRemovable"); Object result = getVolumeList.invoke(mStorageManager); final int length = Array.getLength(result); for (int i = 0; i < length; i++) { Object storageVolumeElement = Array.get(result, i); String path = (String) getPath.invoke(storageVolumeElement); boolean removable = (Boolean) isRemovable.invoke(storageVolumeElement); if (is_removale == removable) { return path; } } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return null; } else { return null; } } /** * 清理 TF 卡上的 h264 文件 * 当 h264 文件总大小超过指定阈值或 TF 卡剩余空间小于指定值时,删除日期最早的文件夹 * * @param context Context 对象,用于获取 TF 卡路径和剩余空间 * @param h264RootDir h264 根目录路径,例如 "/sdcard/h264" * @param maxTotalSizeGB 最大总大小(GB),默认 5GB * @param minFreeSpaceGB 最小剩余空间(GB),默认 1GB */ public static void cleanupH264Files(Context context, String h264RootDir, long maxTotalSizeGB, long minFreeSpaceGB) { if (context == null || h264RootDir == null || h264RootDir.trim().isEmpty()) { Timber.w("Context or h264 root directory is null, skip cleanup"); return; } File h264Root = new File(h264RootDir); if (!h264Root.exists() || !h264Root.isDirectory()) { Timber.d("H264 root directory does not exist: %s, skip cleanup", h264RootDir); return; } try { // 获取 TF 卡路径 String tfCardPath = getStoragePath(context, true); if (tfCardPath == null || tfCardPath.trim().isEmpty()) { Timber.w("TF card path not available, skip cleanup"); return; } // 获取 TF 卡剩余空间(GB) long freeSpaceGB = getFreeSpaceGB(tfCardPath); Timber.d("TF card free space: %d GB", freeSpaceGB); // 扫描所有日期文件夹 File[] dateDirs = h264Root.listFiles(File::isDirectory); if (dateDirs == null || dateDirs.length == 0) { Timber.d("No date directories found in h264 root: %s", h264RootDir); return; } // 按日期排序(最早的在前) List dateDirList = new ArrayList<>(); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA); for (File dateDir : dateDirs) { String dirName = dateDir.getName(); // 只处理符合日期格式的文件夹(yyyyMMdd) if (dirName.length() == 8 && dirName.matches("\\d{8}")) { try { Date date = dateFormat.parse(dirName); long totalSize = calculateH264FilesSize(dateDir); dateDirList.add(new DateDirInfo(dateDir, date, totalSize)); } catch (ParseException e) { Timber.w("Invalid date directory name: %s", dirName); } } } // 按日期排序(最早的在前) Collections.sort(dateDirList, new Comparator() { @Override public int compare(DateDirInfo o1, DateDirInfo o2) { return o1.date.compareTo(o2.date); } }); // 计算所有 h264 文件的总大小(GB) long totalSizeGB = 0; for (DateDirInfo info : dateDirList) { totalSizeGB += info.totalSize / (1024L * 1024L * 1024L); } Timber.d("Total h264 files size: %d GB, Max allowed: %d GB", totalSizeGB, maxTotalSizeGB); Timber.d("TF card free space: %d GB, Min required: %d GB", freeSpaceGB, minFreeSpaceGB); // 检查是否需要清理 boolean needCleanup = (totalSizeGB > maxTotalSizeGB) || (freeSpaceGB < minFreeSpaceGB); if (!needCleanup) { Timber.d("No cleanup needed"); return; } // 删除最早的日期文件夹,直到满足条件 int deletedCount = 0; while (!dateDirList.isEmpty() && ((totalSizeGB > maxTotalSizeGB) || (freeSpaceGB < minFreeSpaceGB))) { DateDirInfo oldestDir = dateDirList.remove(0); Timber.d("Deleting oldest date directory: %s (size: %d GB, date: %s)", oldestDir.dir.getName(), oldestDir.totalSize / (1024L * 1024L * 1024L), dateFormat.format(oldestDir.date)); // 删除文件夹及其所有内容 if (deleteDirectory(oldestDir.dir)) { deletedCount++; totalSizeGB -= oldestDir.totalSize / (1024L * 1024L * 1024L); // 重新获取剩余空间 freeSpaceGB = getFreeSpaceGB(tfCardPath); Timber.d("After deletion - Total size: %d GB, Free space: %d GB", totalSizeGB, freeSpaceGB); } else { Timber.e("Failed to delete directory: %s", oldestDir.dir.getAbsolutePath()); break; // 删除失败,停止清理 } } Timber.i("Cleanup completed. Deleted %d date directories. Final total size: %d GB, Free space: %d GB", deletedCount, totalSizeGB, freeSpaceGB); } catch (Exception e) { Timber.e(e, "Error during h264 files cleanup"); } } /** * 清理 TF 卡上的 h264 文件(使用默认参数:最大5GB,最小剩余空间1GB) */ public static void cleanupH264Files(Context context, String h264RootDir) { cleanupH264Files(context, h264RootDir, 5, 1); } /** * 检查内部 Flash(非 TF 卡)剩余空间,如果小于 800MB,则按时间顺序删除 h264_*.h264 文件 * 目录:context.getExternalFilesDir(null).getAbsolutePath() * 文件名格式示例:h264_1735023032000.h264、h264_camera2_1735023032000.h264 * * 删除规则: * - 按文件名中的时间戳从小到大(越早越先删)依次删除 * - 每删除一次后重新计算剩余空间,直到 ≥ 800MB 或文件删完 * * 返回值: * - 0:最终剩余空间 ≥ 800MB 或无需删除 * - -1:删除完所有符合规则的文件后,剩余空间仍然 < 800MB */ public static int ensureInternalFlashSpaceForH264(Context context) { if (context == null) { Timber.w("ensureInternalFlashSpaceForH264: context is null"); return 0; } File externalDir = context.getExternalFilesDir(null); if (externalDir == null) { Timber.w("ensureInternalFlashSpaceForH264: external files dir is null"); return 0; } String basePath = externalDir.getAbsolutePath(); long minFreeBytes = 800L * 1024L * 1024L; // 800MB long freeBytes = getFreeSpaceBytes(basePath); Timber.d("ensureInternalFlashSpaceForH264: freeBytes=%d, minRequired=%d", freeBytes, minFreeBytes); if (freeBytes >= minFreeBytes) { // 空间充足,无需处理 return 0; } // 收集符合命名规则的 h264 文件 File[] files = externalDir.listFiles(); if (files == null || files.length == 0) { Timber.w("ensureInternalFlashSpaceForH264: no files in dir -> %s", basePath); // 已经没有可删的文件,如果仍小于 800MB,则直接返回 -1 return freeBytes >= minFreeBytes ? 0 : -1; } class H264FileInfo { File file; long timestamp; H264FileInfo(File file, long timestamp) { this.file = file; this.timestamp = timestamp; } } List h264Files = new ArrayList<>(); for (File file : files) { if (!file.isFile()) { continue; } String name = file.getName(); // 只处理 .h264 结尾,且以 h264_ 开头的文件 if (!name.toLowerCase(Locale.CHINA).endsWith(".h264")) { continue; } if (!name.startsWith("h264_") && !name.startsWith("h264_camera2_")) { continue; } // 提取时间戳部分 String timePart = null; if (name.startsWith("h264_camera2_")) { // 前缀长度 13:"h264_camera2_" timePart = name.substring("h264_camera2_".length(), name.length() - ".h264".length()); } else if (name.startsWith("h264_")) { // 前缀长度 5:"h264_" timePart = name.substring("h264_".length(), name.length() - ".h264".length()); } if (timePart == null || timePart.isEmpty()) { continue; } try { long ts = Long.parseLong(timePart); h264Files.add(new H264FileInfo(file, ts)); } catch (NumberFormatException e) { // 文件名不符合时间戳格式,跳过 Timber.w("ensureInternalFlashSpaceForH264: invalid timestamp in file name -> %s", name); } } if (h264Files.isEmpty()) { Timber.w("ensureInternalFlashSpaceForH264: no matched h264 files in -> %s", basePath); return freeBytes >= minFreeBytes ? 0 : -1; } // 按时间戳升序排列(越早的越先删) Collections.sort(h264Files, new Comparator() { @Override public int compare(H264FileInfo o1, H264FileInfo o2) { return Long.compare(o1.timestamp, o2.timestamp); } }); int deletedCount = 0; for (H264FileInfo info : h264Files) { if (freeBytes >= minFreeBytes) { break; } File f = info.file; long size = f.length(); Timber.d("ensureInternalFlashSpaceForH264: deleting file -> %s, size=%d, ts=%d", f.getAbsolutePath(), size, info.timestamp); if (f.delete()) { deletedCount++; // 删除后重新获取剩余空间,更准确 freeBytes = getFreeSpaceBytes(basePath); Timber.d("ensureInternalFlashSpaceForH264: after delete, freeBytes=%d", freeBytes); } else { Timber.e("ensureInternalFlashSpaceForH264: failed to delete file -> %s", f.getAbsolutePath()); } } Timber.i("ensureInternalFlashSpaceForH264: deleted %d files, final freeBytes=%d", deletedCount, freeBytes); return freeBytes >= minFreeBytes ? 0 : -1; } /** * 计算目录下所有 .h264 文件的总大小(字节) */ private static long calculateH264FilesSize(File dir) { long totalSize = 0; File[] files = dir.listFiles(); if (files != null) { for (File file : files) { if (file.isFile() && file.getName().toLowerCase().endsWith(".h264")) { totalSize += file.length(); } } } return totalSize; } /** * 获取指定路径的剩余空间(GB) */ private static long getFreeSpaceGB(String path) { try { StatFs statFs = new StatFs(path); long blockSize = statFs.getBlockSizeLong(); long availableBlocks = statFs.getAvailableBlocksLong(); long freeBytes = availableBlocks * blockSize; return freeBytes / (1024L * 1024L * 1024L); // 转换为 GB } catch (Exception e) { Timber.e(e, "Error getting free space for path: %s", path); return 0; } } /** * 获取指定路径的剩余空间(字节) */ private static long getFreeSpaceBytes(String path) { try { StatFs statFs = new StatFs(path); long blockSize = statFs.getBlockSizeLong(); long availableBlocks = statFs.getAvailableBlocksLong(); return availableBlocks * blockSize; } catch (Exception e) { Timber.e(e, "Error getting free space (bytes) for path: %s", path); return 0; } } /** * 递归删除目录及其所有内容 */ private static boolean deleteDirectory(File dir) { if (dir == null || !dir.exists()) { return true; } if (dir.isDirectory()) { File[] children = dir.listFiles(); if (children != null) { for (File child : children) { if (!deleteDirectory(child)) { return false; } } } } return dir.delete(); } /** * 日期文件夹信息 */ private static class DateDirInfo { File dir; Date date; long totalSize; // 字节 DateDirInfo(File dir, Date date, long totalSize) { this.dir = dir; this.date = date; this.totalSize = totalSize; } } }