android second storage 大杂烩

4.4以前

前一段时间在弄保存路径的问题,需求很简单,内置空间不足之后,如果有外置sd卡的话,要把文件保存在sd卡上。这里就不仔细分辨内存、外存、sdcard、外置sdcard之类的概念了。统一按照官方的说法,sdcard & second storage 。当然这个第二storage的称呼貌似也是4.4之后才出现在官方文档的,记得前公司开发基于4.05等版本时,外置sd卡的路径获取也是中间层添加接口实现的。也就是说之前在手机平台上的第三方应用,根本无法利用统一的系统接口来获取外置sdcard,这其中的历史原因也无需细表。最终采用的是网上找来的版本,原理大概等同于命令行运行mount指令,通过分析返回的字符串,获取最终的storage个数和路径。

private static void getExtSDCardPathsByShell(List<String> paths, String innerSdPath) {
    InputStream is = null;
    InputStreamReader isr = null;
    BufferedReader br = null;
    Random randomTool = new Random();

    try {
        // obtain executed result of command line code of 'mount', to judge
        // whether tfCard exists by the result
        Runtime runtime = Runtime.getRuntime();
        Process process = runtime.exec("mount");
        is = process.getInputStream();
        isr = new InputStreamReader(is);
        br = new BufferedReader(isr);
        String line = null;
        int mountPathIndex = 1;
        while ((line = br.readLine()) != null) {
            // format of sdcard file system: vfat/fuse
            if ((!line.contains("fat") && !line.contains("fuse") && !line
                    .contains("storage"))
                    || line.contains("secure")
                    || line.contains("asec")
                    || line.contains("firmware")
                    || line.contains("shell")
                    || line.contains("obb")
                    || line.contains("legacy") || line.contains("data")) {
                continue;
            }

            String[] parts = line.split(" ");
            int length = parts.length;
            if (mountPathIndex >= length) {
                continue;
            }
            String mountPath = parts[mountPathIndex];
            if (!mountPath.contains("/") || mountPath.contains("data")
                    || mountPath.contains("Data")) {
                continue;
            }
            File mountRoot = new File(mountPath);
            if (!mountRoot.exists() || !mountRoot.isDirectory()
                    || !mountRoot.canWrite()) {
                continue;
            }

            Log.d(TAG, "mountPath " + mountPath);
            boolean equalsToPrimarySD = mountPath.equals(innerSdPath);
            if (equalsToPrimarySD) {
                continue;
            }
            //过滤掉是链接的路径 保证结果的唯一性
            if (isPathExist(paths, mountPath, randomTool)) {
                Log.d(TAG, mountPath + " 被滤掉 可能是链接路径! ");
                continue;
            }

            paths.add(mountPath);
        }

        if (br != null) {
            br.close();
        }

        if (isr != null) {
            isr.close();
        }

        if (is != null) {
            is.close();
        }

    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
  }

没错,我又做了一些手脚。因为在实践过程中有些手机会得到两个以上的结果,比如htc e8 :

/storage/ext_sd/
/storage/sdcard0/
/storage/emulated/legacy

并不是有nx的山寨机支持了两个sd卡,而是其中有两个路径实际上指向的是一个磁盘,大概和linux的链接机制有关。寻觅良久都没有找到一个优雅的方法来解决这个问题,最后只能靠一个土锤的办法来搞定,简单粗暴:

 /**
 * 判断路径是否有重复
 * 起到一个过滤的作用
 *
 * @param pathList
 * @param SDpath
 * @param randomTool
 * @return
 */
private static boolean isPathExist(List<String> pathList, String SDpath, Random randomTool) {
    boolean isExist = false;
    final String testFileName = getTestFileName(SDpath, randomTool);
    if (null == testFileName) {
        Log.e(TAG, "测试文件名获取失败 该路径不使用!");
        return true;
    } else {
        Log.d(TAG, "测试文件名" + testFileName);
    }

    File testFile = new File(SDpath + testFileName);

    try {
        testFile.createNewFile();
    } catch (IOException e) {
        Log.e(TAG, "创建测试文件失败 该路径不使用!");
        e.printStackTrace();
        return true;
    }

    for (String existPath : pathList) {
        File verifyFile = new File(existPath + testFileName);
        if (verifyFile.exists()) {
            isExist = true;
            Log.d(TAG, SDpath + " " + existPath + " is the same location.");
            break;
        }
    }

    if (testFile.exists()) {
        testFile.delete();
    }

    return isExist;
}

嗯,这里面还有创建不同测试文件名就不贴了。当然,要申请文件写权限。运行起来效果还是不错的,但是事情到了4.4以上就起了变化。

4.4

发现问题的起因是根据上边的函数完全检测不到有外置sdcard的存在。调试发现,并不是运行mount指令检查不到而是后面的去重处理建立测试文件失败导致的流程错误。而继续到为什么创建文件失败,答案是,没有权限。

各种查询之后得到的答案是,4.4以上谷歌对外置sdcard做了严格的权限控制,应用只允许在系统制定的目录下读写文件,并且不需要申请权限;公共目录,如相册,只有可读权限。并且提供了获取系统缓存目录的接口:

public abstract File[] getExternalFilesDirs(String type);

该方法封装在Context类中。虽然失去了些许自由有的用总是好的,当然还有个问题就是当应用清理缓存时,保存的文件会被直接干掉,比如第三方相机类的应用拍下来的照片也会如此。但谁让别的位置不让写呢,网上提供了形形色色稀奇古怪的解决办法,但是不需要root并且靠谱的基本没有。然而在测试过程中,事情又发生了变化。

5.0及以上

测试过程中发现,有些机器根据上面的接口根本获取不到路径,甚至直接报空——连内置路径都找不到。阴差阳错的都是5.0以上的机器,于是又开始铺天盖地的寻找解决方案,最后发现5.0提供了另外一个媒体类型的缓存目录可用:

public abstract File[] getExternalMediaDirs();

该方法的抽象接口也在Context类中定义,与上面的接口不同的是,在包名目录下的子目录有所不同,当然应该还有一些媒体库扫描权限的不同。刚好我们是个图片类应用,按说生成了图片也是该给系统知道的,所以使用这个接口还算合适,并且应该优先使用。调通之后,发现有些机器ok,还有些机器获取之后还是无写权限。Google了一番,结果方案没找到,倒是找到了中外同行的抱怨记录和骂娘帖子,默默点赞之后分别找了几台5.0的机器测试context中新加的这两个目录获取方法,有TCL、HTC、三星几个牌子的,得到的结果是各有不同啊。

  1. 返回结果是空的;
  2. 一个有效一个无效的;
  3. 接口照常有,但还是可以随便读写的;
  4. 接口都正常,但还是死活写不进去的;
  5. ……

各大厂商百家争鸣、毫不妥协的做法把android家族的兼容难题展现的淋漓尽致,当然也不排除个别厂家的系统存在bug的可能性,让我等第三方应用手足无措、徒呼奈何。最后只能层级处理,关键算法是:能用就用、用不了看看下一个方法能不能用。

最后,吐槽归吐槽,小胳膊始终是拧不过大腿,忍气吞声还是能用就用吧。

感谢您赏个荷包蛋~