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 卡上的 MP4 文件(例如 AnYun_VIDEO 目录) * 当 MP4 文件总大小超过指定阈值或 TF 卡剩余空间小于指定值时,删除日期最早的文件夹 * * @param context Context 对象,用于获取 TF 卡路径和剩余空间 * @param mp4RootDir MP4 根目录路径,例如 "/storage/XXXX-XXXX/AnYun_VIDEO" * @param maxTotalSizeGB 最大总大小(GB),默认 5GB * @param minFreeSpaceGB 最小剩余空间(GB),默认 1GB */ public static void cleanupH264Files(Context context, String mp4RootDir, long maxTotalSizeGB, long minFreeSpaceGB) { // 注意:为了兼容旧代码,方法名仍然叫 cleanupH264Files,但已经改为清理 MP4 文件 if (context == null || mp4RootDir == null || mp4RootDir.trim().isEmpty()) { Timber.w("Context or mp4 root directory is null, skip cleanup"); return; } File mp4Root = new File(mp4RootDir); if (!mp4Root.exists() || !mp4Root.isDirectory()) { Timber.d("MP4 root directory does not exist: %s, skip cleanup", mp4RootDir); 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 = mp4Root.listFiles(File::isDirectory); if (dateDirs == null || dateDirs.length == 0) { Timber.d("No date directories found in mp4 root: %s", mp4RootDir); return; } // 按日期排序(最早的在前) List dateDirList = new ArrayList<>(); // AnYun_VIDEO 使用的是 yyyy_MM_dd 目录名,例如 2026_01_30 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy_MM_dd", Locale.CHINA); for (File dateDir : dateDirs) { String dirName = dateDir.getName(); // 只处理符合日期格式的文件夹(yyyy_MM_dd,例如 2026_01_30) if (dirName.matches("\\d{4}_\\d{2}_\\d{2}")) { try { Date date = dateFormat.parse(dirName); long totalSize = calculateMp4FilesSize(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 mp4 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 卡上的 MP4 文件(使用默认参数:最大5GB,最小剩余空间1GB) */ public static void cleanupH264Files(Context context, String mp4RootDir) { // 为兼容旧调用保留方法名,内部已改为处理 MP4 cleanupH264Files(context, mp4RootDir, 35, 1); } /** * 检查内部 Flash(非 TF 卡)剩余空间,如果小于 800MB,则按时间顺序删除 AnYun_VIDEO 下最早的 MP4 文件 * 目录结构示例:/sdcard/AnYun_VIDEO/yyMMdd/HHmmss_xxx.mp4 * * 删除规则: * - 递归遍历 AnYun_VIDEO 目录,收集所有 .mp4 文件 * - 按 lastModified 时间从早到晚排序(越早越先删) * - 每删除一次后重新计算剩余空间,直到 ≥ 800MB 或文件删完 * * 返回值: * - 0:最终剩余空间 ≥ 800MB 或无需删除 * - -1:删除完所有符合规则的文件后,剩余空间仍然 < 800MB */ public static int ensureInternalFlashSpaceForH264(Context context) { if (context == null) { Timber.w("ensureInternalFlashSpaceForH264: context is null"); return 0; } // 内部存储根目录(与 VideoFileUtils 中保持一致) File externalRoot = android.os.Environment.getExternalStorageDirectory(); if (externalRoot == null) { Timber.w("ensureInternalFlashSpaceForH264: external storage dir is null"); return 0; } // AnYun_VIDEO 根目录 File anyunRoot = new File(externalRoot, "AnYun_VIDEO"); if (!anyunRoot.exists() || !anyunRoot.isDirectory()) { Timber.w("ensureInternalFlashSpaceForH264: AnYun_VIDEO dir not found -> %s", anyunRoot.getAbsolutePath()); return 0; } String basePath = anyunRoot.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; } // 收集所有 .mp4 文件 List mp4Files = new ArrayList<>(); collectMp4Files(anyunRoot, mp4Files); if (mp4Files.isEmpty()) { Timber.w("ensureInternalFlashSpaceForH264: no mp4 files in dir -> %s", basePath); // 已经没有可删的文件,如果仍小于 800MB,则直接返回 -1 return freeBytes >= minFreeBytes ? 0 : -1; } // 按 lastModified 升序排列(越早的越先删) Collections.sort(mp4Files, new Comparator() { @Override public int compare(File o1, File o2) { return Long.compare(o1.lastModified(), o2.lastModified()); } }); int deletedCount = 0; for (File f : mp4Files) { if (freeBytes >= minFreeBytes) { break; } long size = f.length(); Timber.d("ensureInternalFlashSpaceForH264: deleting mp4 file -> %s, size=%d", f.getAbsolutePath(), size); 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; } /** * 计算目录下所有 .mp4 文件的总大小(字节) */ private static long calculateMp4FilesSize(File dir) { long totalSize = 0; File[] files = dir.listFiles(); if (files != null) { for (File file : files) { if (file.isFile() && file.getName().toLowerCase().endsWith(".mp4")) { 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; } } /** * 递归收集目录下所有 .mp4 文件 */ private static void collectMp4Files(File dir, List outList) { if (dir == null || !dir.exists()) { return; } File[] files = dir.listFiles(); if (files == null) { return; } for (File f : files) { if (f.isDirectory()) { collectMp4Files(f, outList); } else if (f.isFile() && f.getName().toLowerCase(Locale.CHINA).endsWith(".mp4")) { outList.add(f); } } } /** * 获取指定路径的剩余空间(字节) */ 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; } } }