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<DateDirInfo> 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<DateDirInfo>() {
|
@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<H264FileInfo> 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<H264FileInfo>() {
|
@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;
|
}
|
}
|
}
|