Android 老设备存储空间展示:机身存储 + TF 卡容量获取完整实现

在电视、机顶盒、工控机、嵌入式老安卓设备开发中,经常需要展示机身内部存储 + 外置 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 滑块,只作为存储占用进度展示条,禁止用户手动拖动,符合设备系统存储页面交互规范。

相关推荐
yz_aiks1 小时前
IDEA终端配置oh-my-zsh实战:安装、插件与日常使用技巧
java·ide·intellij-idea
java1234_小锋1 小时前
LangChain4j 开发Java Agent智能体- HelloWorld 实现
java·langchain4j
RainCity1 小时前
Java Swing 自定义组件库分享(十)
java·笔记·后端
段ヤシ.1 小时前
回顾Java知识点,面试题汇总Day18(持续更新)
java·网络编程·反射
小yu学编程1 小时前
IDEA 2025版本中如何设置包层级结构
java·ide·intellij-idea·层级结构
YXWik61 小时前
CodeGraph安装及在idea的claude code插件中使用
java·ide·intellij-idea
zzipeng1 小时前
Linux 并发与竞争
java·linux·运维
27669582921 小时前
京东随机变速滑块拼图验证码识别(京东E卡)
java·服务器·前端·python·京东滑块·京东变速滑块·京东e卡绑卡
未若君雅裁2 小时前
ArrayList 源码全解析:动态扩容、数组互转与底层原理
java