针对老旧Android设备开发中存储空间展示的痛点,本文提供了一套完整的解决方案,涵盖TF卡路径获取、容量计算及可视化展示等关键技术点。

在电视、机顶盒等嵌入式设备开发过程中,经常需要同时展示内置存储和外置TF卡的容量信息。传统手机方案仅适配内置存储,无法兼容外置存储路径获取、厂商定制系统特性以及老旧设备的特殊适配需求。
本文提供的解决方案包含完整工具类和业务页面实现:通过反射调用StorageManager隐藏API获取TF卡真实路径与挂载状态,结合StatFs计算总空间和剩余空间。同时兼容厂商系统属性读取、空间倍率换算和存储容量兜底修正,内置进度条UI、容量格式化展示以及无TF卡空态布局,代码可直接在老旧项目、TV和嵌入式设备中复用。
首先在AndroidManifest.xml中添加必要权限:
<!-- SD卡挂载权限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="@dimen/x2"
android:text=""
android:textColor="@color/white"
android:textSize="@dimen/x11" />
<TextView
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginTop="@dimen/x3"
android:background="@drawable/line_gradient" />
</LinearLayout>
<TextView
android:id="@+id/tv_xtkj"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/x4"
android:layout_marginRight="@dimen/x4"
android:text=""
android:textColor="@color/white"
android:textSize="@dimen/x9" />
<SeekBar
android:id="@+id/seekbar1"
android:layout_width="match_parent"
android:layout_height="@dimen/x6"
android:layout_marginLeft="@dimen/x4"
android:layout_marginTop="@dimen/x2"
android:layout_marginRight="@dimen/x4"
android:layout_marginBottom="@dimen/x2"
android:max="255"
android:maxHeight="@dimen/x6"
android:minHeight="@dimen/x6"
android:progressDrawable="@drawable/seekbar_style"
android:thumb="@null"
android:thumbOffset="0dip" />
<TextView
android:id="@+id/tv_xtkj_yy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/x4"
android:layout_marginRight="@dimen/x4"
android:text=""
android:textColor="@color/white"
android:textSize="@dimen/x10" />
<TextView
android:id="@+id/tv_tfk"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/x4"
android:layout_marginTop="@dimen/x15"
android:layout_marginRight="@dimen/x4"
android:text=""
android:textColor="@color/white"
android:textSize="@dimen/x9"
android:visibility="gone" />
<SeekBar
android:id="@+id/seekbar2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/x4"
android:layout_marginTop="@dimen/x2"
android:layout_marginRight="@dimen/x4"
android:layout_marginBottom="@dimen/x2"
android:max="255"
android:maxHeight="@dimen/x6"
android:minHeight="@dimen/x6"
android:progressDrawable="@drawable/seekbar_style2"
android:thumb="@null"
android:thumbOffset="0dip"
android:visibility="gone" />
<TextView
android:id="@+id/tv_tfk_yy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/x4"
android:layout_marginRight="@dimen/x4"
android:text=""
android:textColor="@color/white"
android:textSize="@dimen/x10"
android:visibility="gone" />
<LinearLayout
android:id="@+id/ll_no_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:layout_width="@dimen/x57"
android:layout_height="@dimen/x55"
android:layout_marginTop="@dimen/x15"
android:src="@mipmap/img_kzt"/>
<TextView
android:id="@+id/tv_tips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/x12"
android:layout_marginTop="@dimen/x5"
android:layout_marginLeft="@dimen/x6"
android:layout_marginRight="@dimen/x6"
android:textColor="@color/white"
android:text="暂无内容,请插上TF卡"/>
</LinearLayout>
</LinearLayout>
public class StorageSpaceActivity extends BaseActivity<ActivityStorageSpaceBinding> {
TextView tv_title;
TextView tv_xtkj; //系统空间
SeekBar seekbar1;
TextView tv_xtkj_yy; //系统空间 已用
TextView tv_tfk; //TF卡
SeekBar seekbar2;
TextView tv_tfk_yy; //TF卡 已用
LinearLayout ll_no_data;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_storage_space);
//隐藏状态栏
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
tv_title = findViewById(R.id.tv_title);
tv_title.setText("存储空间");
tv_xtkj = findViewById(R.id.tv_xtkj);
seekbar1 = findViewById(R.id.seekbar1);
tv_xtkj_yy = findViewById(R.id.tv_xtkj_yy);
tv_tfk = findViewById(R.id.tv_tfk);
seekbar2 = findViewById(R.id.seekbar2);
tv_tfk_yy = findViewById(R.id.tv_tfk_yy);
ll_no_data = findViewById(R.id.ll_no_data);
seekbar1.setFocusable(false);
seekbar2.setFocusable(false);
tv_xtkj.setVisibility(View.GONE);
seekbar1.setVisibility(View.GONE);
tv_xtkj_yy.setVisibility(View.GONE);
initdata();
}
@Override
protected void init() {
}
@Override
public int setLayoutID() {
return R.layout.activity_storage_space;
}
private void initdata() {
try {
float total = SDCardUtils.getTotalSize(StorageSpaceActivity.this);
int hongyao = SystemPropertiesProxy.getInt(StorageSpaceActivity.this, SDCardUtils.MEMORY_FR, 0);
//剩余空间
float freeSpace = SDCardUtils.getFreeSize(StorageSpaceActivity.this, hongyao);
Log.e("TAG", "total " + total);
Log.e("TAG", "hongyao " + hongyao);
Log.e("TAG", "freeSpace " + freeSpace);
tv_xtkj.setText("系统空间(可用" + freeSpace + ")");
//已使用
float usemem = (float) (Math.round((total - freeSpace) * 100)) / 100;
tv_xtkj_yy.setText("已用" + usemem + "/" + total);
seekbar1.setMax((int) (total));
seekbar1.setProgress((int) (usemem));
} catch (Exception e) {
//内存路径
String innert = Environment.getExternalStorageDirectory().getPath();
//总空间
long total = SDCardUtils.getTotalInternalMemorySize(innert);
if(total > 20L * 1024L * 1024L * 1024L){
total = 32L * 1024L * 1024L * 1024L;
}else if(total > 10L * 1024L * 1024L * 1024L){
total = 16L * 1024L * 1024L * 1024L;
}else if(total > 6L * 1024L * 1024L * 1024L){
total = 8L * 1024L * 1024L * 1024L;
}else if(total > 4L * 1024L * 1024L * 1024L){
total = 6L * 1024L * 1024L * 1024L;
}else {
total = 4L * 1024L * 1024L * 1024L;
}
//剩余空间
long freeSpace = SDCardUtils.getFreeSpace(innert);
Log.e("TAG", "total2 " + total);
Log.e("TAG", "freeSpace2 " + freeSpace);
tv_xtkj.setText("系统空间(可用" + Formatter.formatFileSize(StorageSpaceActivity.this, freeSpace) + ")");
//已使用
long usemem = total - freeSpace;
tv_xtkj_yy.setText("已用" + Formatter.formatFileSize(StorageSpaceActivity.this, usemem) + "/" + Formatter.formatFileSize(StorageSpaceActivity.this, total));
seekbar1.setMax((int) (total / 1024));
seekbar1.setProgress((int) (usemem / 1024));
}
//SD卡路径
String sDcardDir = SDCardUtils.getTfStorageDirectory(this);
if (!TextUtils.isEmpty(sDcardDir)) {
//总空间
long total2 = SDCardUtils.getTotalInternalMemorySize(sDcardDir);
if(total2 > 0){
tv_tfk.setVisibility(View.VISIBLE);
seekbar2.setVisibility(View.VISIBLE);
tv_tfk_yy.setVisibility(View.VISIBLE);
}
//剩余空间
long freeSpace2 = SDCardUtils.getFreeSpace(sDcardDir);
Log.e("TAG", "total3 " + total2);
Log.e("TAG", "freeSpace3 " + freeSpace2);
tv_tfk.setText("TF卡(可用" + Formatter.formatFileSize(StorageSpaceActivity.this, freeSpace2) + ")");
//已使用
long sdusemem = total2 - freeSpace2;
tv_tfk_yy.setText("已用" + Formatter.formatFileSize(StorageSpaceActivity.this, sdusemem) + "/" + Formatter.formatFileSize(StorageSpaceActivity.this, total2));
seekbar2.setMax((int) (total2 / 1024));
seekbar2.setProgress((int) (sdusemem / 1024));
}else {
ll_no_data.setVisibility(View.VISIBLE);
}
}
}
public class SDCardUtils {
private static final String TYPE_CACHE = "cache";
private static final String TYPE_FILES = "files";
private static String sTfDir = "";
private static final String MEMORY_ROM = "persist.sys.memory_rom";//总内存属性
public static final String MEMORY_FR = "ro.sys.memory_rom.fr"; //真假空间属性
/**
* SD卡是否挂载
*
* @return
*/
public static boolean isMounted() {
String status = Environment.getExternalStorageState();
return status.equals(Environment.MEDIA_MOUNTED) ? true : false;
}
//判断是否存在外置tf卡
public static boolean isStorageTf(Context context) {
try {
StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
Method getVolumeStateMethod = StorageManager.class.getMethod("getVolumeState", new Class[]{String.class});
String state = (String) getVolumeStateMethod.invoke(sm, getTfStoragePath(context));
if (state.equals(Environment.MEDIA_MOUNTED)) {
return true;
}
} catch (Exception e) {
Log.e("TF error", "not find tf", e);
}
return false;
}
//获取外置tf卡路径
public static String getTfStoragePath(Context context) {
try {
@SuppressLint("WrongConstant")
StorageManager sm = (StorageManager) context.getSystemService("storage");
Method getVolumePathsMethod = StorageManager.class.getMethod("getVolumePaths", new Class[0]);
String[] paths = (String[]) getVolumePathsMethod.invoke(sm, new Object[]{});
// second element in paths[] is secondary storage path
return paths[1];
} catch (Exception e) {
Log.e("TF error", "get Tf failed", e);
}
return null;
}
/**
* 获取tf卡根目录
* @param context
* @return
*/
public static String getExternalStorageDirectoryPath(Context context) {
sTfDir = getTfStorageDirectory(context);
return sTfDir;
}
/**
* 获取tf卡根目录
* @param context
* @return
*/
public static String getTfStorageDirectory(Context context) {
String tfDir = null;
StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
Class<?> smc = sm.getClass();
try {
Method getPaths = smc.getMethod("getVolumePaths", new Class[0]);
String[] paths = (String[])getPaths.invoke(sm, new Object[]{});
if (paths.length >= 2) {
tfDir = paths[1];
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return tfDir;
}
/**
* 获取手机内部总的存储空间
* @return
*/
public static long getTotalInternalMemorySize(String rootPath) {
StatFs stat = new StatFs(rootPath);
long blockSize = stat.getBlockSizeLong();
long totalBlocks = stat.getBlockCountLong();
return (totalBlocks * blockSize);
}
/**
* 获取剩余内存空间
*/
public static long getFreeSpace(String rootPath) {
StatFs stat = new StatFs(rootPath);
//获取单个数据块的大小(Byte)
long blockSize = stat.getBlockSizeLong();
//空闲的数据块的数量
long availableBlocks = stat.getAvailableBlocksLong();
//单位Byte
return availableBlocks * blockSize;
}
public static float getFreeSize(Context context,int isTrue){
float freeSize = getAvailableSpace(context);
if(isTrue == 1){
freeSize = freeSize * 2;
}
return freeSize;
}
public static float getTotalSize(Context context ){
String free_memory = SystemPropertiesProxy.get(context,MEMORY_ROM);
float freeSize = Float.parseFloat(free_memory);
return freeSize;
}
public static float getAvailableSpace(Context context){
String path = "/storage/emulated/0";
StatFs statFs = new StatFs(path);
long blockSize = statFs.getBlockSizeLong();
long availableBlocks = statFs.getAvailableBlocksLong();
long ava_length = availableBlocks*blockSize;
float f = Float.parseFloat(String.valueOf(ava_length));
float available = (float)(Math.round(((f/1024/1024/1024)-0.01)*100))/100;
return (float) (available) ;
}
}
MOUNT_UNMOUNT_FILESYSTEMS是老设备文件挂载必备权限,虽然在高版本中被标记为受保护权限,但在机顶盒和老旧安卓设备上仍需声明才能正常读取TF卡状态。
getTfStoragePath和getTfStorageDirectory方法通过反射StorageManager的隐藏方法getVolumePaths获取第二分区路径作为TF卡路径。isStorageTf方法则通过反射getVolumeState判断TF卡是否正常挂载,这是嵌入式设备的标准实现方式。
使用StatFs获取块大小、总块数和可用块数,通过计算得到总容量和剩余容量,再使用系统Formatter自动适配GB/MB单位进行格式化展示。
persist.sys.memory_rom
ro.sys.memory_rom.fr
针对厂商定制系统常见的虚拟内存特性,通过读取系统属性获取标称总容量和空间倍率标识,在代码中实现动态倍率换算,确保适配各种魔改系统。
通过设置focusable=false和移除滑块,将SeekBar改造为只读的存储占用进度展示条,符合设备系统存储页面的交互规范。
本文提供的解决方案全面覆盖了老旧Android设备存储空间展示的各类技术难点,从底层API调用到UI展示都做了充分适配,可直接应用于各类嵌入式设备开发场景。