如何获取Android 4.0+的外部SD卡路径?

95

三星 Galaxy S3具有外部SD卡插槽,它被挂载到 /mnt/extSdCard

我该如何像Environment.getExternalStorageDirectory()这样获取此路径?

这将返回mnt/sdcard,我找不到用于外部SD卡的API。(或某些平板电脑上的可移动USB存储器。)


为什么你想要这样做?在Android上做这样的事情是非常不可取的。也许如果你分享一下你的动机,我们可以指导你朝着最佳实践的方向前进。 - rharter
2
当用户更换手机时,将SD卡插入新手机,在第一个手机上它是/sdcard,在第二个手机上它是/mnt/extSdCard,任何使用文件路径的东西都会崩溃。我需要从相对路径生成真实路径。 - Romulus Urakagi Ts'ai
我知道这个话题有点老了,但这可能会有所帮助。你应该使用这种方法。System.getenv();请参考Environment3项目以访问连接到您设备的所有存储。https://github.com/omidfaraji/Environment3 - Omid Faraji
可能是查找外部SD卡位置的重复问题。 - Brian Rogers
这是我的解决方案,适用于Nougat版本之前:https://dev59.com/Npbfa4cB1Zd3GeqPrk7J#40205116 - Gokul NC
27个回答

59

我找到的解决方案有一些变化,源自这里

public static HashSet<String> getExternalMounts() {
    final HashSet<String> out = new HashSet<String>();
    String reg = "(?i).*vold.*(vfat|ntfs|exfat|fat32|ext3|ext4).*rw.*";
    String s = "";
    try {
        final Process process = new ProcessBuilder().command("mount")
                .redirectErrorStream(true).start();
        process.waitFor();
        final InputStream is = process.getInputStream();
        final byte[] buffer = new byte[1024];
        while (is.read(buffer) != -1) {
            s = s + new String(buffer);
        }
        is.close();
    } catch (final Exception e) {
        e.printStackTrace();
    }

    // parse output
    final String[] lines = s.split("\n");
    for (String line : lines) {
        if (!line.toLowerCase(Locale.US).contains("asec")) {
            if (line.matches(reg)) {
                String[] parts = line.split(" ");
                for (String part : parts) {
                    if (part.startsWith("/"))
                        if (!part.toLowerCase(Locale.US).contains("vold"))
                            out.add(part);
                }
            }
        }
    }
    return out;
}

原始的方法已经测试过并适用于:

  • Huawei X3(原装系统)
  • Galaxy S2(原装系统)
  • Galaxy S3(原装系统)

我不确定当时进行测试时这些设备运行的 Android 版本。

我已经使用以下设备测试了我的修改版:

  • Moto Xoom 4.1.2(原装系统)
  • 使用 otg 线的 Galaxy Nexus(cyanogenmod 10)
  • HTC Incredible(cyanogenmod 7.2),这个设备返回了内存和扩展存储。它是一个奇怪的设备,因为 getExternalStorage() 返回的是 sdcard 的路径而不是其内存。

以及一些将 sd 卡作为主要存储设备的单卡设备:

  • HTC G1(cyanogenmod 6.1)
  • HTC G1(原装系统)
  • HTC Vision/G2(原装系统)

除了 Incredible 外,所有这些设备只返回其可移动存储。我可能需要进行一些额外的检查,但这至少比我目前找到的任何解决方案都要好一点。


3
如果原始文件夹名称包含大写字母,例如 /mnt/SdCard,我认为您的解决方案将提供无效的文件夹名称。 - Eugene Popovich
1
我已经在一些摩托罗拉、三星和LG设备上进行了测试,它完美地工作了。 - Jonathan Toledo
2
@KhawarRaza,这个方法在KitKat版本之后停止工作了。请使用Dmitriy Lozenko的方法,并将此作为备选方案,以支持早期的ICS设备。 - Gnathonic
1
适用于华为荣耀3c。谢谢。 - Weiyi
2
这对我在Galaxy Note 3上的4.0+版本返回/mnt而不是/storage。 - ono
显示剩余10条评论

54

我找到了一种更可靠的方法来获取系统中所有SD卡的路径。该方法适用于所有Android版本,并返回所有存储(包括模拟存储)的路径。

在我所有的设备上都可以正常工作。

P.S.:基于Environment类的源代码。

private static final Pattern DIR_SEPORATOR = Pattern.compile("/");

/**
 * Raturns all available SD-Cards in the system (include emulated)
 *
 * Warning: Hack! Based on Android source code of version 4.3 (API 18)
 * Because there is no standart way to get it.
 * TODO: Test on future Android versions 4.4+
 *
 * @return paths to all available SD-Cards in the system (include emulated)
 */
public static String[] getStorageDirectories()
{
    // Final set of paths
    final Set<String> rv = new HashSet<String>();
    // Primary physical SD-CARD (not emulated)
    final String rawExternalStorage = System.getenv("EXTERNAL_STORAGE");
    // All Secondary SD-CARDs (all exclude primary) separated by ":"
    final String rawSecondaryStoragesStr = System.getenv("SECONDARY_STORAGE");
    // Primary emulated SD-CARD
    final String rawEmulatedStorageTarget = System.getenv("EMULATED_STORAGE_TARGET");
    if(TextUtils.isEmpty(rawEmulatedStorageTarget))
    {
        // Device has physical external storage; use plain paths.
        if(TextUtils.isEmpty(rawExternalStorage))
        {
            // EXTERNAL_STORAGE undefined; falling back to default.
            rv.add("/storage/sdcard0");
        }
        else
        {
            rv.add(rawExternalStorage);
        }
    }
    else
    {
        // Device has emulated storage; external storage paths should have
        // userId burned into them.
        final String rawUserId;
        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)
        {
            rawUserId = "";
        }
        else
        {
            final String path = Environment.getExternalStorageDirectory().getAbsolutePath();
            final String[] folders = DIR_SEPORATOR.split(path);
            final String lastFolder = folders[folders.length - 1];
            boolean isDigit = false;
            try
            {
                Integer.valueOf(lastFolder);
                isDigit = true;
            }
            catch(NumberFormatException ignored)
            {
            }
            rawUserId = isDigit ? lastFolder : "";
        }
        // /storage/emulated/0[1,2,...]
        if(TextUtils.isEmpty(rawUserId))
        {
            rv.add(rawEmulatedStorageTarget);
        }
        else
        {
            rv.add(rawEmulatedStorageTarget + File.separator + rawUserId);
        }
    }
    // Add all secondary storages
    if(!TextUtils.isEmpty(rawSecondaryStoragesStr))
    {
        // All Secondary SD-CARDs splited into array
        final String[] rawSecondaryStorages = rawSecondaryStoragesStr.split(File.pathSeparator);
        Collections.addAll(rv, rawSecondaryStorages);
    }
    return rv.toArray(new String[rv.size()]);
}

5
DIR_SEPORATOR 应该是什么? - cYrixmorten
1
@cYrixmorten 它提供在代码的顶部.. 它是一个用于 /Pattern - Ankit Bansal
有人在Lolipop上测试过这个解决方案吗? - sdf3qrxewqrxeqwxfew3123
我在联想P780上尝试了这种方法,但它并没有起作用。 - Satheesh Kumar
2
仅适用于棒棒糖;在棉花糖上无法正常工作(Galaxy Tab A w/ 6.0.1),其中 System.getenv("SECONDARY_STORAGE") 返回 /storage/sdcard1。那个位置不存在,但实际位置是 /storage/42CE-FD0A,其中 42CE-FD0A 是已格式化分区的卷序列号。 - DaveAlden
显示剩余3条评论

32

我猜想要使用外部SD卡,你需要使用这个:

new File("/mnt/external_sd/")

或者

new File("/mnt/extSdCard/")

对于你的情况...

替换 Environment.getExternalStorageDirectory()

这对我有效。你应该首先检查mnt目录中的内容,然后从那里开始操作。


你应该使用某种选择方法来选择要使用的SD卡:

File storageDir = new File("/mnt/");
if(storageDir.isDirectory()){
    String[] dirList = storageDir.list();
    //TODO some type of selecton method?
}

1
实际上,我想要一个方法而不是硬编码路径,但/mnt/列表应该可以。谢谢。 - Romulus Urakagi Ts'ai
没问题。在设置中有一个选项,你可以选择路径,然后使用该方法来检索它。 - FabianCook
10
不幸的是,外部SD卡可能位于与/mnt不同的文件夹中。例如,在三星GT-I9305上,它位于/storage/extSdCard,而内置SD卡的路径为/storage/sdcard0。 - iseeall
2
那么您将获得sdcard位置,然后获取其父目录。 - FabianCook
Android 4.0+系统中,通过OTG线连接到平板电脑的U盘的外部SD卡路径是什么? - osimer pothe

17

我曾使用 Dmitriy Lozenko 的解决方案,但在 Asus Zenfone2, Marshmallow 6.0.1 上测试时发现该解决方案无法工作。具体而言,当获取微型SD卡路径 /storage/F99C-10F4/ 时,该解决方案会失败。我修改了代码,直接从模拟应用程序路径中获取模拟根路径,并添加了更多已知的特定于手机型号的物理路径:context.getExternalFilesDirs(null);

为了让我们的生活更加轻松,我创建了一个库here。您可以通过gradle、maven、sbt和leiningen构建系统使用它。

如果您喜欢老式的方法,也可以直接从here复制粘贴文件,但您将无法在未手动检查更新的情况下知道是否有更新。

如果您有任何问题或建议,请告诉我


1
我没有广泛使用过这个,但是它对我的问题很有效。 - David
1
很棒的解决方案!不过需要注意的是,如果在应用程序处于活动状态时移除SD卡,则context.getExternalFilesDirs(null)将为其中一个文件返回null,并且在接下来的循环中会出现异常。我建议在循环的第一行添加if (file == null) continue; - Koger
1
@Koger 谢谢您的建议,我已经将其添加并修正了我的回答中的一个错别字。 - HendraWD

15

为了获取所有外部存储(无论它们是SD卡还是内部不可移除存储),您可以使用以下代码:

final String state = Environment.getExternalStorageState();

if ( Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state) ) {  // we can read the External Storage...           
    //Retrieve the primary External Storage:
    final File primaryExternalStorage = Environment.getExternalStorageDirectory();

    //Retrieve the External Storages root directory:
    final String externalStorageRootDir;
    if ( (externalStorageRootDir = primaryExternalStorage.getParent()) == null ) {  // no parent...
        Log.d(TAG, "External Storage: " + primaryExternalStorage + "\n");
    }
    else {
        final File externalStorageRoot = new File( externalStorageRootDir );
        final File[] files = externalStorageRoot.listFiles();

        for ( final File file : files ) {
            if ( file.isDirectory() && file.canRead() && (file.listFiles().length > 0) ) {  // it is a real directory (not a USB drive)...
                Log.d(TAG, "External Storage: " + file.getAbsolutePath() + "\n");
            }
        }
    }
}

或者,您可以使用System.getenv("EXTERNAL_STORAGE")来检索主要的外部存储目录(例如"/storage/sdcard0"),并使用System.getenv("SECONDARY_STORAGE")来检索所有次要目录的列表(例如"/storage/extSdCard:/storage/UsbDriveA:/storage/UsbDriveB")。请注意,在这种情况下,您可能需要过滤次要目录列表以排除USB驱动器。

无论如何,请注意,使用硬编码路径始终是一种不好的方法(尤其是当每个制造商都可以按照自己的意愿更改路径时)。


System.getenv("SECONDARY_STORAGE")在Nexus 5和Nexus 7上连接OTG电缆的USB设备上无法正常工作。 - Khawar Raza
这是我的解决方案,适用于Nougat版本:https://dev59.com/Npbfa4cB1Zd3GeqPrk7J#40205116 - Gokul NC

13

好消息!在KitKat中,现在有一个公共API可以与这些次要共享存储设备进行交互。

新的Context.getExternalFilesDirs()Context.getExternalCacheDirs()方法可以返回多个路径,包括主设备和次要设备。您可以遍历它们并检查Environment.getStorageState()File.getFreeSpace()以确定存储文件的最佳位置。这些方法也可在支持v4库中的ContextCompat上使用。

还要注意,如果您只想使用由Context返回的目录,则不再需要READ_WRITE_EXTERNAL_STORAGE权限。从现在开始,您将始终可以读/写访问这些目录,无需额外的权限。

应用程序也可以通过结束生命周期请求权限来继续在旧设备上工作,例如:

<uses-permission
    android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="18" />

谢谢。我已经编写了一些代码,遵循您所写的内容,并且我认为它在查找所有SD卡路径方面运行良好。您能看一下吗?这里是链接:stackoverflow.com/a/27197248/878126 。顺便说一句,您不应该使用“getFreeSpace”,因为理论上,在运行时可用空间可能会发生变化。 - android developer
请参考以下链接:https://developer.android.com/reference/android/content/Context.html#getExternalFilesDirs(java.lang.String) - Strubbl
在 Android 26 Pixel 1 模拟器上,它不会返回 SD 卡(可移动),只有像 X-plore 和 Solid Explorer 这样的应用程序才能看到此卡和默认文件应用程序。 - user924

12
我为了获得对所有外部SD卡的访问权限而采取了以下步骤。
使用:

File primaryExtSd=Environment.getExternalStorageDirectory();

你可以获取到主要外部SD卡的路径,然后使用以下代码:

File parentDir=new File(primaryExtSd.getParent());

您可以获取主外部存储的父目录,它也是所有外部SD卡的父目录。现在,您可以列出所有存储并选择您想要的存储。

希望这对您有所帮助。


这应该是被接受的答案,谢谢,它很有用。 - Meanman
我的SD卡位于/storage/FSAB-2345/。primaryExtSd显示为“/storage/emulated/0”,但那实际上是我的内部存储!所以我必须使用primaryExtSd.getParentFile().getParentFile()来到达“/storage”,那是我的外部存储目录的父目录。在到达根目录后,似乎需要对整个树进行完整遍历,才能真正找到所有存储位置。 - safe_malloc

11

发现一种更正式的方法,从Android N开始(如果在此之前,您可以尝试我上面写的方法),特别是从Android R开始,使用StorageManager(基于我在这里所提供的解决方案):

class MainActivity : AppCompatActivity() {
    @RequiresApi(Build.VERSION_CODES.N)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        getSdCardPaths(this, true)?.forEach { volumePath ->
            Log.d("AppLog", "volumePath:$volumePath")
        }
    }

    /**
     * returns a list of all available sd cards paths, or null if not found.
     *
     * @param includePrimaryExternalStorage set to true if you wish to also include the path of the primary external storage
     */
    fun getSdCardPaths(context: Context, includePrimaryExternalStorage: Boolean): List<String>? {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
            val storageVolumes = storageManager.storageVolumes
            if (!storageVolumes.isNullOrEmpty()) {
                val primaryVolume = storageManager.primaryStorageVolume
                val result = ArrayList<String>(storageVolumes.size)
                for (storageVolume in storageVolumes) {
                    val volumePath = getVolumePath(storageVolume) ?: continue
                    if (storageVolume.uuid == primaryVolume.uuid || storageVolume.isPrimary) {
                        if (includePrimaryExternalStorage)
                            result.add(volumePath)
                        continue
                    }
                    result.add(volumePath)
                }
                return if (result.isEmpty()) null else result
            }
        }
        val externalCacheDirs = ContextCompat.getExternalCacheDirs(context)
        if (externalCacheDirs.isEmpty())
            return null
        if (externalCacheDirs.size == 1) {
            if (externalCacheDirs[0] == null)
                return null
            val storageState = EnvironmentCompat.getStorageState(externalCacheDirs[0])
            if (Environment.MEDIA_MOUNTED != storageState)
                return null
            if (!includePrimaryExternalStorage && Environment.isExternalStorageEmulated())
                return null
        }
        val result = ArrayList<String>()
        if (externalCacheDirs[0] != null && (includePrimaryExternalStorage || externalCacheDirs.size == 1))
            result.add(getRootOfInnerSdCardFolder(context, externalCacheDirs[0]))
        for (i in 1 until externalCacheDirs.size) {
            val file = externalCacheDirs[i] ?: continue
            val storageState = EnvironmentCompat.getStorageState(file)
            if (Environment.MEDIA_MOUNTED == storageState)
                result.add(getRootOfInnerSdCardFolder(context, externalCacheDirs[i]))
        }
        return if (result.isEmpty()) null else result
    }

    fun getRootOfInnerSdCardFolder(context: Context, inputFile: File): String {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
            storageManager.getStorageVolume(inputFile)?.let {
                val result = getVolumePath(it)
                if (result != null)
                    return result
            }
        }
        var file: File = inputFile
        val totalSpace = file.totalSpace
        while (true) {
            val parentFile = file.parentFile
            if (parentFile == null || parentFile.totalSpace != totalSpace || !parentFile.canRead())
                return file.absolutePath
            file = parentFile
        }
    }

    @RequiresApi(Build.VERSION_CODES.N)
    fun getVolumePath(storageVolume: StorageVolume): String? {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
            return storageVolume.directory?.absolutePath
        try {
            val storageVolumeClazz = StorageVolume::class.java
            val getPath = storageVolumeClazz.getMethod("getPath")
            return getPath.invoke(storageVolume) as String
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return null
    }
}

文档(http://developer.android.com/reference/android/content/Context.html#getExternalCacheDirs())中提到:“此处返回的外部存储设备被视为设备的永久部分,包括模拟的外部存储和物理介质插槽,例如电池仓中的SD卡。返回的路径不包括瞬态设备,例如USB闪存驱动器。”听起来它们似乎也不包括SD卡插槽,即您可以在不打开电池盖的情况下取出SD卡的位置。不过很难确定。 - LarsH
1
@LarsH 嗯,我已经在SGS3上(和真实的SD卡)测试过了,所以我认为它应该也适用于其他设备。 - android developer
@LarsH 试试我提供的新代码,也许会有帮助。 :) - android developer
通过反射调用StorageVolume.getPath()在目标SDK 30上不再起作用。 - Alexey Ozerov
@AlexeyOzerov 代码在模拟器API 30和API 31上运行良好,并且已经检查版本是否来自Android R(API 30),这将不使用反射,那么问题到底是什么? - android developer
显示剩余2条评论

5

感谢大家提供的线索,尤其是 @SmartLemon,我找到了解决方案。如果还有其他人需要,我将我的最终解决方案放在这里(查找第一个列出的外部 SD 卡):

public File getExternalSDCardDirectory()
{
    File innerDir = Environment.getExternalStorageDirectory();
    File rootDir = innerDir.getParentFile();
    File firstExtSdCard = innerDir ;
    File[] files = rootDir.listFiles();
    for (File file : files) {
        if (file.compareTo(innerDir) != 0) {
            firstExtSdCard = file;
            break;
        }
    }
    //Log.i("2", firstExtSdCard.getAbsolutePath().toString());
    return firstExtSdCard;
}

如果没有外部SD卡,则返回内置存储。如果SD卡不存在,我将使用它,您可能需要进行更改。

1
在Android版本19上会生成null,在rootDir.listFiles()时返回null。我已经使用Nexus 7,模拟器和Galaxy Note进行了测试。 - Manu Zi

3

请参考我的代码,希望对您有所帮助:

    Runtime runtime = Runtime.getRuntime();
    Process proc = runtime.exec("mount");
    InputStream is = proc.getInputStream();
    InputStreamReader isr = new InputStreamReader(is);
    String line;
    String mount = new String();
    BufferedReader br = new BufferedReader(isr);
    while ((line = br.readLine()) != null) {
        if (line.contains("secure")) continue;
        if (line.contains("asec")) continue;

        if (line.contains("fat")) {//TF card
            String columns[] = line.split(" ");
            if (columns != null && columns.length > 1) {
                mount = mount.concat("*" + columns[1] + "\n");
            }
        } else if (line.contains("fuse")) {//internal storage
            String columns[] = line.split(" ");
            if (columns != null && columns.length > 1) {
                mount = mount.concat(columns[1] + "\n");
            }
        }
    }
    txtView.setText(mount);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接