You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Java应用扫描外部存储目录时无法检出xml等文本文件

OpenCV Haar级联测试Android应用外部存储文本文件识别问题

问题场景

正在开发一款用于测试OpenCV haarcascades的Android Java应用,遇到无法识别外部存储下.xml文件的问题。

已声明权限

Manifest中已添加如下权限:

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

核心遍历代码

用于获取外部存储指定目录下文件名与路径的代码实现如下:

public ArrayList<String []> getLongAndShortFileHandles(String root_dir) {
    ArrayList<String[]> ret = new ArrayList<>();
    String path = Environment.getExternalStorageDirectory().toString()+"/"+root_dir;
    File directory = new File(path);
    File[] files = directory.listFiles();
    Log.e(TAG, "getLongAndShortFileHandles: Has dir "+path+", with "+((files==null) ? "null" : files.length) + " number of files");
    for (int i = 0; i < Objects.requireNonNull(files).length; i++) {
        Log.e(TAG, "getLongAndShortFileHandles: HAS FILE "+files[i].getName());
        String [] temp = new String [] {files[i].getAbsolutePath(),files[i].getName()};
        ret.add(temp);
    }
    return ret;
}

调用逻辑

通过上述方法填充自定义RecyclerView子类的调用代码如下:

this.cascade_selector = (RecyclerView) findViewById(R.id.cascade_view);
this.cascade_selector.setAdapter(new FileAdaptah(getLongAndShortFileHandles("cascade_app/cascades"),this, CASCADE));

this.xml_ano_selector = (RecyclerView) findViewById(R.id.annotation_view);
this.xml_ano_selector.setAdapter(new FileAdaptah(getLongAndShortFileHandles("cascade_app/annotations"),this, ANNOTATION_XML));

故障现象

运行应用后仅能显示第二次调用(对应cascade_app/annotations目录)的结果,第一次调用(对应cascade_app/cascades目录)无内容输出。将第一次调用的目录替换为DCIM、Music等其他系统目录,可正常显示文件,但只要任意目录下添加.xml文件,该文件就不会被显示。

排查记录

  • 补充1:测试不同目录与文件后发现,问题不止出在.xml文件上,所有可测试的ASCII编码文本格式文件都无法被检出;而同目录下的.jpg、.wav、.mp4、.bin等二进制文件都能正常被识别。
  • 补充2:验证发现如果将任意文本文件的扩展名改为二进制类扩展名(例如将50.xml重命名为50.jpg),文件就能被遍历逻辑检测到。尝试了不同的文件名过滤器,未解决问题;也尝试改用Directory Stream实现文件遍历,同样无效,对应代码如下:
    @RequiresApi(api = Build.VERSION_CODES.O)
    public ArrayList<String[]> getFiles(String suffix, String root_dir){
        ArrayList<String[]> ret = new ArrayList<String[]>();
        try {
            root_dir = Environment.getExternalStorageDirectory().toString() + "/"+ root_dir;
            Path dirName = Paths.get(root_dir);
            DirectoryStream<Path> paths = Files.newDirectoryStream(dirName, "*.pdf");
            paths.forEach(path -> {
                Log.e(TAG, "getFiles: HAS FILE : "+path.toString());
                if (path.endsWith(suffix)) {
                    String sPath = path.toString();
                    String[] splitted = sPath.split(File.separator);
                    ret.add(new String[]{sPath, splitted[splitted.length - 1]});
                }
            });
        } catch (IOException exception) {
            exception.printStackTrace();
        }
        return ret;
    }
    
  • 补充3:注意到上述getFiles方法的代码中遗留了之前复制时的*.pdf glob表达式,尝试修改或移除该表达式,问题仍然存在。
  • 补充4:编写测试代码直接访问目标文件,对应代码如下:
    this.test = (Button) findViewById(R.id.select_to_test);
    this.test.setOnClickListener(v -> {
        String path = Environment.getExternalStorageDirectory().toString()+"/cascade_app/cascades/50.xml";
        Intent intent = new Intent(v.getContext(),CheckCascade.class);
        intent.putExtra("fh",path);
        File debugFile = new File(path);
        Toast.makeText(this, "File "+path+" does "+((debugFile.exists())? "exist":"not exist"), Toast.LENGTH_SHORT).show();
        startActivity(intent);
    });
    
    测试发现直接通过File.exists()判断可以正常检测到该.xml文件的存在。

开发环境

  • Android Studio版本:4.1.2
  • target SDK:30
  • minSdk:19
  • 测试设备:运行Android 11的三星Galaxy A40

问题原因

该现象并非系统框架Bug,是Android 10+分区存储(Scoped Storage)机制在三星定制OneUI系统上的特殊表现,对应行为逻辑如下:

  • 当应用targetSdk >=29(项目targetSdk为30)时,默认启用分区存储规则,无法再通过传统的Environment.getExternalStorageDirectory()路径下的File.listFiles()方法,直接枚举公共目录下所有类型的文件
  • 三星OneUI对该规则的实现做了定制:枚举目录时,仅返回已被系统MediaStore索引的二进制媒体类型文件(图片、音频、视频、通用二进制),所有被归类为文档/文本类的文件(.xml、.txt、.json等)会被从返回的文件列表中过滤掉
  • 直接通过完整全路径构造File对象调用exists()/canRead()时,系统会直接走底层文件系统校验,不会触发枚举过滤逻辑,因此可以正常检测到目标文件存在,和补充4的测试结果完全吻合
  • 改用Directory Stream遍历无效,是因为该API同样走应用层目录枚举逻辑,会被系统的过滤规则拦截

解决方案

按照适配成本从低到高可选择以下方案:

1. 临时调试方案

仅用于开发阶段快速验证逻辑,不适合正式发布:
在Manifest的<application>节点下添加属性,强制应用请求旧版存储权限,绕过部分分区存储限制:

<application
    android:requestLegacyExternalStorage="true"
    android:preserveLegacyExternalStorage="true">
    ...
</application>

注意:该方案在Android 11及以上的三星设备上无法完全生效,系统依然会对文本类文件做枚举过滤,仅能作为临时调试手段

2. 全版本兼容的官方适配方案

放弃直接通过文件系统路径枚举外部存储目录的逻辑,改用系统提供的存储访问框架(SAF)让用户手动选择目标级联文件目录,通过持久化Uri权限遍历目录下所有类型的文件,不会被系统过滤。
核心逻辑参考:

  • 启动目录选择器,让用户选中指定的cascade_app/cascades和cascade_app/annotations目录
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    startActivityForResult(intent, REQUEST_CODE_SELECT_DIR);
    
  • 拿到用户授予的目录Uri后,通过ContentResolver查询目录下所有子文件,无需区分文件类型
    Uri treeUri = resultData.getData();
    Uri docUri = DocumentsContract.buildDocumentUriUsingTree(treeUri,
            DocumentsContract.getTreeDocumentId(treeUri));
    // 查询目录下所有子文件
    try (Cursor cursor = getContentResolver().query(
            DocumentsContract.buildChildDocumentsUriUsingTree(docUri,
                    DocumentsContract.getDocumentId(docUri)),
            new String[]{
                    DocumentsContract.Document.COLUMN_DISPLAY_NAME,
                    DocumentsContract.Document.COLUMN_DOCUMENT_ID
            },
            null, null, null)) {
        // 遍历cursor即可拿到所有文件的名称和访问Uri,包含文本类文件
    }
    
  • 调用takePersistableUriPermission保存用户授予的目录权限,应用重启后无需用户重复选择。

3. OpenCV场景最优适配方案

完全不需要申请外部存储读取权限:将需要测试的haarcascade xml文件直接放到应用的assets目录下,OpenCV的CascadeClassifier支持直接加载本地文件流,不存在系统存储限制问题。
示例加载逻辑:

// 将assets下的级联文件拷贝到应用私有存储目录
File cascadeFile = new File(getExternalFilesDir(null), "haarcascade_frontalface.xml");
InputStream is = getAssets().open("haarcascades/haarcascade_frontalface.xml");
FileOutputStream fos = new FileOutputStream(cascadeFile);
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
    fos.write(buffer, 0, len);
}
fos.close();
is.close();
// 加载级联分类器
CascadeClassifier classifier = new CascadeClassifier(cascadeFile.getAbsolutePath());

内容的提问来源于stack exchange,提问作者Nikolai Savulkin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.25 13:36:06