Android 设备存储一般分成内置存储(自身ROM)和外置存储,外置存储设备大致就两种,即 SD 卡和 U 盘,本篇将介绍如何获取外置存储设备的路径、读取文件列表和监听其插拔状态。
一、文件读写基本知识
Android 中文件读写的方式一般有如下三种
Android 定义了以下与存储相关的权限
READ_EXTERNAL_STORAGE
WRITE_EXTERNAL_STORAGE
MANAGE_EXTERNAL_STORAGE
:MANAGE_EXTERNAL_STORAGE
Android 13 后增加的细分媒体权限
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
一般持有MANAGE_EXTERNAL_STORAGE
权限的即可访问绝大多数文件路径,例外情况Android/data
、Android/obb
访问私有应用文件目录,只有使用SAF方式访问,需要用户授权目录。
另外OTG存储,无法通过MediaStore API方式查询其媒体文件,但是可以查询内置存储和SD卡存储。
二、存储卷读取
storageManager.getVolumes()
和 StorageManager.getVolumeList()
都是用于获取设备上所有存储卷的方法,但它们之间存在一些关键区别:
1. 返回值:
storageManager.getVolumes()
返回一个List<VolumeInfo>
对象,其中包含有关每个存储卷的信息,例如卷名、路径、类型等。StorageManager.getVolumeList()
返回一个StorageVolume[]
数组,其中包含每个存储卷的引用。
2. 可访问性:
storageManager.getVolumes()
可以访问所有类型的存储卷,包括内部存储、外部存储和 USB 存储。StorageManager.getVolumeList()
只能访问可公开访问的存储卷,例如外部存储。(测试USB 存储无法读取
)
3. 权限:
storageManager.getVolumes()
需要READ_EXTERNAL_STORAGE
权限才能访问外部存储和 USB 存储。StorageManager.getVolumeList()
不需要任何权限。
4. 版本:
storageManager.getVolumes()
是 Android 5.0 (API 21) 中引入的新方法。StorageManager.getVolumeList()
是旧方法,在 Android 4.0 (API 14) 中引入。
表格总结了 storageManager.getVolumes()
和 StorageManager.getVolumeList()
之间的区别:
特性 | storageManager.getVolumes() | StorageManager.getVolumeList() |
---|---|---|
返回值 | List<VolumeInfo> | StorageVolume[] |
可访问性 | 所有类型 | 可公开访问 |
权限 | READ_EXTERNAL_STORAGE | 无 |
版本 | Android 5.0 (API 21) | Android 4.0 (API 14) |
- 如果您需要访问所有类型的存储卷,并可以使用
READ_EXTERNAL_STORAGE
权限,请使用storageManager.getVolumes()
方法。 - 如果您只需要访问可公开访问的存储卷,或者不想使用任何权限,请使用
StorageManager.getVolumeList()
方法。
三、 VolumeInfo和StorageVolume属性及其含义
VolumeInfo 和 StorageVolume 是 Android 系统中用于表示存储卷的两个类。它们之间存在一些相似之处,但也有一些关键区别。
VolumeInfo 类包含有关存储卷的更多信息,例如卷的状态和 UUID,而 StorageVolume 类只包含基本信息,例如卷名、路径、类型等。
以下是 VolumeInfo 和 StorageVolume 类的属性及其含义:
VolumeInfo 类属性:
- _id: 卷 ID
- type: 卷类型,例如内部存储、外部存储、USB 存储等
- state: 卷状态,例如挂载、卸载、不可访问等
- mountPoint: 卷挂载路径
- fsUuid: 卷文件系统 UUID
- partGuid: 卷分区 GUID
- flags: 卷标志,例如只读、可移动等
- ownerUid: 卷所有者用户 ID
- primaryDir: 卷主目录
- volumeId: 卷 ID
StorageVolume 类属性:
- volumeId: 卷 ID
- fsType: 卷文件系统类型
- state: 卷状态,例如挂载、卸载、不可访问等
- path: 卷挂载路径
- isPrimary: 是否为主要存储卷
- isEmulated: 是否为模拟存储卷
- maxFileSize: 卷上最大文件大小
以下是一些属性的含义:
-
type:
指示存储卷的类型。可能的类型包括:
TYPE_INTERNAL
: 内部存储TYPE_EXTERNAL
: 外部存储TYPE_PUBLIC
: 公共存储TYPE_USB
: USB 存储TYPE_SDCARD
: SD 卡
-
state:
指示存储卷的状态。可能的 state 包括:
MEDIA_MOUNTED
: 存储卷已挂载MEDIA_UNMOUNTED
: 存储卷已卸载MEDIA_BAD_REMOVAL
: 存储卷被不正确移除MEDIA_NOFS
: 存储卷没有文件系统MEDIA_CHECKING
: 存储卷正在检查
-
mountPoint: 指示存储卷的挂载路径。
-
fsUuid: 指示存储卷的文件系统 UUID。
-
partGuid: 指示存储卷的分区 GUID。
-
flags:
指示存储卷的标志。可能的标志包括:
FLAG_READ_ONLY
: 存储卷只读FLAG_REMOVABLE
: 存储卷可移动
-
ownerUid: 指示存储卷的所有者用户 ID。
-
primaryDir: 指示存储卷的主目录。
-
volumeId: 指示存储卷的 ID。
-
fsType: 指示存储卷的文件系统类型。
-
isPrimary: 指示存储卷是否为主要存储卷。
-
isEmulated: 指示存储卷是否为模拟存储卷。
-
maxFileSize: 指示存储卷上最大文件大小。
四、输出存储卷代码
输出打印存储卷代码段:
public void getStorageVolume(Context context) {
StorageManager mStorageManagerr = ((StorageManager) context.getSystemService(Context.STORAGE_SERVICE));
// 使用 storageManager.getVolumes() 获取所有存储卷
List<VolumeInfo> volumeInfos = mStorageManager.getVolumes();
for (VolumeInfo volumeInfo : volumeInfos) {
android.util.Log.e("maxx", "getVolumes volumeInfo:" + volumeInfo.toString());
}
// 使用 StorageManager.getVolumeList() 获取所有可公开访问的存储卷
StorageVolume[] storageVolumes = mStorageManager.getVolumeList();
ArrayList<StorageVolume> mountVolumeList = new ArrayList<>();
for (StorageVolume storageVolume : storageVolumes) {
android.util.Log.e("maxx", "getVolumeList storageVolume:" + storageVolume.dump());
}
}
输出打印存储卷信息:
M144CA9 03-12 17:50:36.871 22035 22035 E maxx : getVolumes volumeInfo:VolumeInfo{public:8,49}:
M144CA9 03-12 17:50:36.871 22035 22035 E maxx : type=PUBLIC diskId=disk:8,48 partGuid= mountFlags=0 mountUserId=0
M144CA9 03-12 17:50:36.871 22035 22035 E maxx : state=MOUNTED
M144CA9 03-12 17:50:36.871 22035 22035 E maxx : fsType=vfat fsUuid=BEA6-BBCE fsLabel=
M144CA9 03-12 17:50:36.871 22035 22035 E maxx : path=/mnt/media_rw/BEA6-BBCE internalPath=/mnt/media_rw/BEA6-BBCE
M144CA9 03-12 17:50:36.871 22035 22035 E maxx : linkName=usbdisk
M144CAA 03-12 17:50:36.871 22035 22035 E maxx : getVolumes volumeInfo:VolumeInfo{private}:
M144CAA 03-12 17:50:36.871 22035 22035 E maxx : type=PRIVATE diskId=null partGuid=null mountFlags=0 mountUserId=-10000
M144CAA 03-12 17:50:36.871 22035 22035 E maxx : state=MOUNTED
M144CAA 03-12 17:50:36.871 22035 22035 E maxx : fsType=null fsUuid=null fsLabel=null
M144CAA 03-12 17:50:36.871 22035 22035 E maxx : path=/data internalPath=null linkName=unknown
M144CAB 03-12 17:50:36.871 22035 22035 E maxx : getVolumes volumeInfo:VolumeInfo{public:179,1}:
M144CAB 03-12 17:50:36.871 22035 22035 E maxx : type=PUBLIC diskId=disk:179,0 partGuid= mountFlags=VISIBLE mountUserId=0
M144CAB 03-12 17:50:36.871 22035 22035 E maxx : state=MOUNTED
M144CAB 03-12 17:50:36.871 22035 22035 E maxx : fsType=vfat fsUuid=4819-161B fsLabel=
M144CAB 03-12 17:50:36.871 22035 22035 E maxx : path=/storage/4819-161B internalPath=/mnt/media_rw/4819-161B
M144CAB 03-12 17:50:36.871 22035 22035 E maxx : linkName=sdcard0
M144CAC 03-12 17:50:36.871 22035 22035 E maxx : getVolumes volumeInfo:=VolumeInfo{emulated;0}:
M144CAC 03-12 17:50:36.871 22035 22035 E maxx : type=EMULATED diskId=null partGuid= mountFlags=PRIMARY|VISIBLE
M144CAC 03-12 17:50:36.871 22035 22035 E maxx : mountUserId=0 state=MOUNTED
M144CAC 03-12 17:50:36.871 22035 22035 E maxx : fsType=null fsUuid=null fsLabel=null
M144CAC 03-12 17:50:36.871 22035 22035 E maxx : path=/storage/emulated internalPath=/data/media linkName=
M144CAD 03-12 17:50:36.871 22035 22035 E maxx : getVolumeList storageVolume:StorageVolume:
M144CAD 03-12 17:50:36.871 22035 22035 E maxx : mId=emulated;0 mPath=/storage/emulated/0 mInternalPath=/storage/emulated/0
M144CAD 03-12 17:50:36.871 22035 22035 E maxx : mDescription=内部共享存储空间 mPrimary=true mRemovable=false mEmulated=true
M144CAD 03-12 17:50:36.871 22035 22035 E maxx : mAllowMassStorage=false mMaxFileSize=0 mOwner=UserHandle{0} mFsUuid=null
M144CAD 03-12 17:50:36.871 22035 22035 E maxx : mState=mounted
M144CCE 03-12 17:50:36.931 22035 22035 E maxx : getVolumeList storageVolume=StorageVolume:
M144CCE 03-12 17:50:36.931 22035 22035 E maxx : mId=public:179,1 mPath=/storage/4819-161B
M144CCE 03-12 17:50:36.931 22035 22035 E maxx : mInternalPath=/mnt/media_rw/4819-161B mDescription=SanDisk SD 卡
M144CCE 03-12 17:50:36.931 22035 22035 E maxx : mPrimary=false mRemovable=true mEmulated=false mAllowMassStorage=false
M144CCE 03-12 17:50:36.931 22035 22035 E maxx : mMaxFileSize=4294967295 mOwner=UserHandle{0} mFsUuid=4819-161B
M144CCE 03-12 17:50:36.931 22035 22035 E maxx : mState=mounted
如何判断是SD卡还是OTG还是内部存储,根据输出的Log可以分析,内部存储的 type=EMULATED 并且 diskId=null,而SD卡和OTG类型都是 type=PUBLIC,并且 diskId 不为空。
所以通过volumeInfo
的话,一般volumeInfo.getType() == VolumeInfo.TYPE_PRIVATE
或者volumeInfo.getType() == VolumeInfo.TYPE_EMULATED
就是内部存储,我遇到有些设备这种判断还是不准确。volumeInfovol.getDisk() != null && vol.getDisk().isUsb()
就是OTG,volumeInfo.getDisk() != null && sv.getDisk().isSd()
就是SD卡。
大部分情况下,如果要获取所有存储卷,我们都是使用mStorageManager.getVolumes()
,但是通过打印的Log,可以发现storageVolume
会多一个属性mDescription
用于描述设备的内容,一般会包含设备的品牌名,如何从VolumeInfo
中获取描述设备属性呢?
String mDescription = mStorageManager.getBestVolumeDescription(volumeInfo);
我们还可以通过VolumeInfo
获取存储设备的根目录,通过直接文件路径遍历文件目录
public static String ROOT_PATH = "/storage/emulated/0";
String path = (volume.getType() == VolumeInfo.TYPE_PRIVATE && VolumeInfo.ID_PRIVATE_INTERNAL.equals(volume.getId())
|| volume.getType() == VolumeInfo.TYPE_EMULATED)
? ROOT_PATH
: volume.getPath().toString();
五、监听插拔状态
5.1 监听SD卡插拔
// 监听广播
private final BroadcastReceiver mSDReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case Intent.ACTION_MEDIA_CHECKING://SD卡正在检查
case Intent.ACTION_MEDIA_MOUNTED://SD卡挂载成功
android.util.Log.e("maxx","mSDReceiver mounted");
break;
case Intent.ACTION_MEDIA_EJECT://SD卡拔出
case Intent.ACTION_MEDIA_UNMOUNTED://SD卡卸载成功
android.util.Log.e("maxx","mSDReceiver eject");
break;
default:
break;
}
}
};
// 注册监听
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_MEDIA_CHECKING);
intentFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
intentFilter.addAction(Intent.ACTION_MEDIA_EJECT);
intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
registerReceiver(mSDReceiver, intentFilter);
// 取消监听
unregisterReceiver(mScannerReceiver);
5.2 监听OTG插拔
网上很多说使用UsbManager.ACTION_USB_DEVICE_ATTACHED
和UsbManager.ACTION_USB_DEVICE_DETACHED
这二个广播可以监听到USB设备插拔,但是我使用OTG的设备插拔无法接受到此广播。
后面使用VolumeInfo.ACTION_VOLUME_STATE_CHANGED
才可以监听到,而且可以查看设备有没有准备好,有没有mounted或者unmounted。
VolumeInfo
:
public static final String ACTION_VOLUME_STATE_CHANGED =
"android.os.storage.action.VOLUME_STATE_CHANGED";
public static final String EXTRA_VOLUME_ID =
"android.os.storage.extra.VOLUME_ID";
public static final String EXTRA_VOLUME_STATE =
"android.os.storage.extra.VOLUME_STATE";
// 监听广播
public final BroadcastReceiver mDiskReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals(VolumeInfo.ACTION_VOLUME_STATE_CHANGED) ){
int id = intent.getIntExtra(VolumeInfo.EXTRA_VOLUME_ID, 0);
int state =intent.getIntExtra(VolumeInfo.EXTRA_VOLUME_STATE, VolumeInfo.STATE_UNMOUNTABLE);
android.util.Log.e("maxx","mDiskReceiver id:"+id+" state="+state);
}
}
};
//注册监听
registerReceiver(mDiskReceiver, new IntentFilter(VolumeInfo.ACTION_VOLUME_STATE_CHANGED));
//取消监听
unregisterReceiver(mDiskReceiver);
相关参考
[1] USB 存储媒介