在电视、机顶盒、工控机、嵌入式老安卓设备开发中,经常需要展示机身内部存储 + 外置 TF 卡存储空间,并通过进度条可视化已用 / 可用容量。 普通手机方案只适配内置存储,无法兼容外置 TF 卡路径获取、厂商定制系统存储空间属性、以及老设备容量适配兜底。
本文封装完整工具类 + 完整业务页面:通过反射调用 StorageManager 隐藏 API 获取 TF 卡真实路径与挂载状态,结合 StatFs 计算总空间、剩余空间;同时兼容厂商系统属性读取、真假空间倍率换算、存储空间档位兜底修正,自带进度条 UI、容量格式化展示、无 TF 卡空态布局,代码可直接在老项目、TV、嵌入式设备中复用。
首先在AndroidManifest.xml添加权限
xml
<!-- 在SDCard 的挂载权限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
布局文件
ini
<?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>
java代码
ini
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);
}
}
}
工具类
ini
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) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
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) ;
}
}
代码解释:
1. 权限说明
MOUNT_UNMOUNT_FILESYSTEMS 是老设备文件挂载必备权限,高版本虽标记为受保护,但机顶盒 / 老安卓依然需要声明才能正常读取 TF 卡状态。
2. 反射获取 TF 卡路径 & 挂载状态
getTfStoragePath / getTfStorageDirectory
isStorageTf
原生 Android 没有公开获取外置 TF 卡路径的 API,通过反射 StorageManager 隐藏方法 getVolumePaths ,拿到第二分区路径即为 TF 卡;再反射 getVolumeState 判断是否正常挂载,是嵌入式设备标准写法。
3. StatFs 存储空间计算原理
通过 StatFs 获取块大小、总块数、可用块数,换算成总容量 / 剩余容量 ,再用系统 Formatter 自动适配 GB/MB 单位格式化展示。
4. 厂商系统属性适配
persist.sys.memory_rom
ro.sys.memory_rom.fr
很多定制设备会做虚拟真假内存,通过系统属性读取标称总容量、空间倍率标识,代码里做了倍率动态换算,适配厂商魔改系统。
5. SeekBar 作为进度条禁用拖动
设置 focusable=false、去掉 thumb 滑块,只作为存储占用进度展示条,禁止用户手动拖动,符合设备系统存储页面交互规范。