查找可移动SD卡的位置

219

有没有一种通用的方法来查找外部SD卡的位置?

请不要与外部存储混淆。

Environment.getExternalStorageState()返回内部SD挂载点的路径,例如/mnt/sdcard。但是问题是关于外部SD卡的。我怎么才能获得像/mnt/sdcard/external_sd这样的路径(它可能因设备而异)?

我想我最终会通过文件系统名称过滤mount命令的输出。但我不确定这种方法是否足够健壮。


这是我的解决方案,适用于Nougat版本:https://dev59.com/Npbfa4cB1Zd3GeqPrk7J#40205116 - Gokul NC
"Environment.getExternalStorageState() 返回指向内部SD挂载点的路径,例如“/mnt/sdcard”。嗯,从Android的角度来说,它并不是内部的。我相信你要找的术语是“不可移除的”(not removable)。" - LarsH
1
adb shell echo $EXTERNAL_STORAGE - Charles L.
25个回答

171

Environment.getExternalStorageState() 返回内部SD卡挂载点的路径,例如“/mnt/sdcard”

不是的,Environment.getExternalStorageDirectory() 指的是设备制造商认为的“外部存储”。在某些设备上,这是可移动介质,例如SD卡;在某些设备上,这是设备内部闪存的一部分。这里,“外部存储”指的是“通过USB大容量存储模式在主机上挂载时可以访问的内容”,至少适用于Android 1.x和2.x。

但问题是关于外部SD卡的。如何获取像“/mnt/sdcard/external_sd”这样的路径(它可能因设备而异)?

Android没有“外部SD卡”的概念,除了上面描述的外部存储。

如果设备制造商选择将外部存储放在内置闪存上并且还有一个SD卡,则需要联系该制造商以确定是否可以使用SD卡(不保证),以及使用它的规则,例如要使用什么路径。


更新

两个最近值得注意的事情:

首先,在Android 4.4+上,您无法写入可移动介质(例如“外部SD卡”),除非该介质上可能由getExternalFilesDirs()getExternalCacheDirs()返回的任何位置。请参见Dave Smith的出色分析,特别是如果您想了解低级细节。

其次,为了避免任何人对可移动介质访问是否属于Android SDK进行争论,在此Dianne Hackborn做了评估

请记住:在Android 4.4之前,官方的Android平台根本不支持SD卡,除了两种特殊情况:旧的存储布局中外部存储是SD卡(该平台仍然支持),以及在Android 3.0中添加的一个小功能,它会扫描额外的SD卡并将其添加到媒体提供程序中,并使应用程序只能读取其文件(该平台今天仍然支持)。
Android 4.4是该平台实际允许应用程序使用SD卡存储的第一个版本。在此之前的任何访问都是通过私有的、不受支持的API进行的。现在我们在平台上拥有了一个非常丰富的API,允许应用程序以支持的方式使用SD卡,比以前更好地利用它们的应用程序特定存储区域,而无需在应用程序中需要任何权限,并且可以访问SD卡上的任何其他文件,只要它们通过文件选择器即可,也无需任何特殊权限。

4
随着 HC 和 ICS 设备的推出,这个问题变得越来越严重,因为它们将“ExternalStorageDirectory”和其他类似的指向内部物理存储。更糟糕的是,大多数用户根本不知道如何在文件系统中找到他们的 sdcard。 - Tony Maro
308
你的回答基本上是“联系制造商”,这并没有什么用。 - dragonroot
6
回答的最后一部分并不太准确--实际上,可以通过按照下面的答案(扫描/proc/mounts、/system/etc/vold.fstab等)来检测SD卡路径。 - Learn OpenGL ES
9
尽管如此,仍然不准确地认为一个人“需要”联系制造商,因为有许多适用于多种设备的解决方案,而且SDK本身在所有设备上都不一致,因此不能保证解决。即使这些解决方案不适用于所有设备,它们仍可适用于足够多的设备,以至于市场上许多Android应用程序依赖这些技术(以及其他技术)来检测外部SD卡路径。我认为称所有这些开发者为傻瓜有点苛刻和过早——难道顾客不是最终的评判者吗? - Learn OpenGL ES
5
作为事情进展的一部分,那很公平。我完全同意您的观点,开发人员不能假设这将在所有地方都起作用,并且不能保证任何此类代码将适用于所有Android设备或版本。希望它能在SDK中得到修复!与此同时,仍然有一些选项可以在许多设备上运行并改善最终用户体验,在80%成功和0%成功之间选择,我会选择80%。 - Learn OpenGL ES
显示剩余11条评论

65

我根据在这里找到的一些答案,想出了以下解决方案。

代码:

public class ExternalStorage {

    public static final String SD_CARD = "sdCard";
    public static final String EXTERNAL_SD_CARD = "externalSdCard";

    /**
     * @return True if the external storage is available. False otherwise.
     */
    public static boolean isAvailable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
            return true;
        }
        return false;
    }

    public static String getSdCardPath() {
        return Environment.getExternalStorageDirectory().getPath() + "/";
    }

    /**
     * @return True if the external storage is writable. False otherwise.
     */
    public static boolean isWritable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state)) {
            return true;
        }
        return false;

    }

    /**
     * @return A map of all storage locations available
     */
    public static Map<String, File> getAllStorageLocations() {
        Map<String, File> map = new HashMap<String, File>(10);

        List<String> mMounts = new ArrayList<String>(10);
        List<String> mVold = new ArrayList<String>(10);
        mMounts.add("/mnt/sdcard");
        mVold.add("/mnt/sdcard");

        try {
            File mountFile = new File("/proc/mounts");
            if(mountFile.exists()){
                Scanner scanner = new Scanner(mountFile);
                while (scanner.hasNext()) {
                    String line = scanner.nextLine();
                    if (line.startsWith("/dev/block/vold/")) {
                        String[] lineElements = line.split(" ");
                        String element = lineElements[1];

                        // don't add the default mount path
                        // it's already in the list.
                        if (!element.equals("/mnt/sdcard"))
                            mMounts.add(element);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            File voldFile = new File("/system/etc/vold.fstab");
            if(voldFile.exists()){
                Scanner scanner = new Scanner(voldFile);
                while (scanner.hasNext()) {
                    String line = scanner.nextLine();
                    if (line.startsWith("dev_mount")) {
                        String[] lineElements = line.split(" ");
                        String element = lineElements[2];

                        if (element.contains(":"))
                            element = element.substring(0, element.indexOf(":"));
                        if (!element.equals("/mnt/sdcard"))
                            mVold.add(element);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }


        for (int i = 0; i < mMounts.size(); i++) {
            String mount = mMounts.get(i);
            if (!mVold.contains(mount))
                mMounts.remove(i--);
        }
        mVold.clear();

        List<String> mountHash = new ArrayList<String>(10);

        for(String mount : mMounts){
            File root = new File(mount);
            if (root.exists() && root.isDirectory() && root.canWrite()) {
                File[] list = root.listFiles();
                String hash = "[";
                if(list!=null){
                    for(File f : list){
                        hash += f.getName().hashCode()+":"+f.length()+", ";
                    }
                }
                hash += "]";
                if(!mountHash.contains(hash)){
                    String key = SD_CARD + "_" + map.size();
                    if (map.size() == 0) {
                        key = SD_CARD;
                    } else if (map.size() == 1) {
                        key = EXTERNAL_SD_CARD;
                    }
                    mountHash.add(hash);
                    map.put(key, root);
                }
            }
        }

        mMounts.clear();

        if(map.isEmpty()){
                 map.put(SD_CARD, Environment.getExternalStorageDirectory());
        }
        return map;
    }
}

用法:

Map<String, File> externalLocations = ExternalStorage.getAllStorageLocations();
File sdCard = externalLocations.get(ExternalStorage.SD_CARD);
File externalSdCard = externalLocations.get(ExternalStorage.EXTERNAL_SD_CARD);

1
已经测试过了,包括 Nexus 4、Nexus S、Galaxy S2、Galaxy S3 和 HTC Desire =) - Richard
2
再次问候,理查德 - 信不信由你,我必须问一下:你是否真的尝试过以这种方式编写并读取文件,而不仅仅是获取目录?还记得我们旧的“/sdcard0”问题吗?我尝试了这段代码,但在我尝试读回它所写的文件时,在S3上失败了。...这非常奇怪...也很痛苦 :)) - Howard Pautz
10
这在没有2张SD卡的设备上会失败。它假设第一张找到的是内部的,第二张找到的是外部的... - Caner
4
在Android 4.3+中,/system/etc/vold.fstab不可访问。 - Ali
这对于在三星平板和PQ Labs iStick之间进行转换非常有效。 - MechEngineer
显示剩余7条评论

38

我有一个应用程序,其中使用了ListPreference,用户需要选择他们希望保存某物的位置。

在该应用中,我扫描了/proc/mounts/system/etc/vold.fstab以获取SD卡的挂载点。我将每个文件中的挂载点存储到两个单独的ArrayList中。

然后,我将一个列表与另一个列表进行比较,并丢弃两个列表中都不存在的项。这给了我每个SD卡的根路径列表。

从那里,我使用File.exists()File.isDirectory()File.canWrite()测试这些路径。如果其中任何一个测试失败,我就会将该路径从列表中丢弃。

剩下的内容,我将其转换为String[]数组,以便可以通过ListPreference的values属性使用。

您可以在这里查看代码。


@fifth,三星声称我们需要将“/external_sd/”附加到Environment.getExternalStorageDirectory()返回的字符串中,然后再附加“/filename.foo”。唯一的问题是,在我测试的三个S3设备上,这并不起作用 - 但它在他们的RemoteTestLab的“真实”设备上起作用。这让我很烦恼... - Howard Pautz
好的,但是如果考虑“最坏情况”呢?比如你有一台三星手机,它有一个内部存储 /sdcard,一个物理的32GB microSD卡 /external_sd/,然后你又插上了一个USB硬盘和一个SSHFS网络挂载。这段代码能找到所有四个设备吗? SSH挂载的参考链接:https://github.com/l3iggs/android_external_sshfs - RoundSparrow hilltx
对我也很有用,但我要提醒一下:该代码检查目录是否可写,如果您的应用程序是只读的(因此不请求写入权限),则需要稍微修改代码。 - Eugene Styer

26

您可以尝试使用支持库函数 ContextCompat.getExternalFilesDirs()

      final File[] appsDir=ContextCompat.getExternalFilesDirs(getActivity(),null);
      final ArrayList<File> extRootPaths=new ArrayList<>();
      for(final File file : appsDir)
        extRootPaths.add(file.getParentFile().getParentFile().getParentFile().getParentFile());

第一个是主要的外部存储器,其余的应该是真正的SD卡路径。

为什么会有多个“ .getParentFile()”呢?因为原始路径需要向上跳到另一个文件夹。

.../Android/data/YOUR_APP_PACKAGE_NAME/files/

编辑:这是我创建的更全面的方法,用于获取SD卡的路径:

  /**
   * 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
   */
  @TargetApi(Build.VERSION_CODES.HONEYCOMB)
  public static List<String> getSdCardPaths(final Context context, final boolean includePrimaryExternalStorage)
    {
    final File[] externalCacheDirs=ContextCompat.getExternalCacheDirs(context);
    if(externalCacheDirs==null||externalCacheDirs.length==0)
      return null;
    if(externalCacheDirs.length==1)
      {
      if(externalCacheDirs[0]==null)
        return null;
      final String storageState=EnvironmentCompat.getStorageState(externalCacheDirs[0]);
      if(!Environment.MEDIA_MOUNTED.equals(storageState))
        return null;
      if(!includePrimaryExternalStorage&&VERSION.SDK_INT>=VERSION_CODES.HONEYCOMB&&Environment.isExternalStorageEmulated())
        return null;
      }
    final List<String> result=new ArrayList<>();
    if(includePrimaryExternalStorage||externalCacheDirs.length==1)
      result.add(getRootOfInnerSdCardFolder(externalCacheDirs[0]));
    for(int i=1;i<externalCacheDirs.length;++i)
      {
      final File file=externalCacheDirs[i];
      if(file==null)
        continue;
      final String storageState=EnvironmentCompat.getStorageState(file);
      if(Environment.MEDIA_MOUNTED.equals(storageState))
        result.add(getRootOfInnerSdCardFolder(externalCacheDirs[i]));
      }
    if(result.isEmpty())
      return null;
    return result;
    }

  /** Given any file/folder inside an sd card, this will return the path of the sd card */
  private static String getRootOfInnerSdCardFolder(File file)
    {
    if(file==null)
      return null;
    final long totalSpace=file.getTotalSpace();
    while(true)
      {
      final File parentFile=file.getParentFile();
      if(parentFile==null||parentFile.getTotalSpace()!=totalSpace||!parentFile.canRead())
        return file.getAbsolutePath();
      file=parentFile;
      }
    }

编辑:更好的解决方案在这里:

https://dev59.com/zGgu5IYBdhLWcg3wfHI_#27197248


1
这似乎是一个非常好的答案,但如何将其集成到简单的活动中呢?有几个变量没有定义,例如 AppContextCompactEnvironmentCompact - Antonio
@Antonio,ContextCompact和EnvironmentCompact可以通过支持库获得。 "App.global()"只是应用程序上下文,我已经全局设置了它,因为我不喜欢在每个地方都添加Context参数。 - android developer
1
太好了!适用于我的设备 v4.4 三星 GT S Advance,希望其他人也能使用。 - user25
@androiddeveloper 修改后的答案是否适用于所有设备和SD卡尺寸? - Rahulrr2602
1
这对我来说完美地解决了问题 - 应该被采纳为正确答案。 - Paradox
显示剩余11条评论

17

为了检索所有的外部存储(无论是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驱动器。

无论如何,请注意使用硬编码路径始终是不好的方法(尤其是当每个制造商都可以随心所欲地更改它时)。


2
只要考虑到任何不留下评论的踩贴者都是巨魔,所以我点赞来弥补它。;) 但是,我猜你的方法相当武断:我们怎么知道跳过那些“USB驱动器”,但保留其他所有内容确实等同于问题中所要求的“sdcards”呢?此外,你建议的 System.getenv("SECONDARY_STORAGE") 也需要一些参考资料,因为它似乎没有文档记录。 - Sz.
1
据我所知,在Android API中没有标准方法来检索所有外部存储设备。 然而,所提出的方法并不是随意的。在Android中,就像在每个Unix/Linux系统中一样,所有挂载的存储设备都存储/链接在一个公共目录下:“/mnt”(用于挂载存储设备的标准Unix/Linux目录)或者在最新版本中是“/storage”。这就是为什么你可以相当确定你会在这个文件夹中找到所有的SD卡链接。 - Paolo Rovelli
1
关于System.getenv(“EXTERNAL_STORAGE”)方法,除了API页面(该页面并没有解释很多内容)之外,我没有任何参考资料:http://developer.android.com/reference/java/lang/System.html#getenv%28java.lang.String%29 我找不到任何官方页面来获取Android系统环境变量。但是,在这里,您可以找到它们的简短列表:http://www.herongyang.com/Android/System-Information-System-Environment-Variables.html - Paolo Rovelli
1
我明白了。是的,你说得对。使用我的方法,您还将检索内部(不可移动)SD存储器。 - Paolo Rovelli
System.getenv("SECONDARY_STORAGE")在Nexus 5和Nexus 7上连接OTG电缆的USB设备上无法正常工作。 - Khawar Raza
显示剩余2条评论

16

我也像 Richard 一样使用 /proc/mounts 文件获取可用存储选项列表。

public class StorageUtils {

    private static final String TAG = "StorageUtils";

    public static class StorageInfo {

        public final String path;
        public final boolean internal;
        public final boolean readonly;
        public final int display_number;

        StorageInfo(String path, boolean internal, boolean readonly, int display_number) {
            this.path = path;
            this.internal = internal;
            this.readonly = readonly;
            this.display_number = display_number;
        }

        public String getDisplayName() {
            StringBuilder res = new StringBuilder();
            if (internal) {
                res.append("Internal SD card");
            } else if (display_number > 1) {
                res.append("SD card " + display_number);
            } else {
                res.append("SD card");
            }
            if (readonly) {
                res.append(" (Read only)");
            }
            return res.toString();
        }
    }

    public static List<StorageInfo> getStorageList() {

        List<StorageInfo> list = new ArrayList<StorageInfo>();
        String def_path = Environment.getExternalStorageDirectory().getPath();
        boolean def_path_internal = !Environment.isExternalStorageRemovable();
        String def_path_state = Environment.getExternalStorageState();
        boolean def_path_available = def_path_state.equals(Environment.MEDIA_MOUNTED)
                                    || def_path_state.equals(Environment.MEDIA_MOUNTED_READ_ONLY);
        boolean def_path_readonly = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED_READ_ONLY);
        BufferedReader buf_reader = null;
        try {
            HashSet<String> paths = new HashSet<String>();
            buf_reader = new BufferedReader(new FileReader("/proc/mounts"));
            String line;
            int cur_display_number = 1;
            Log.d(TAG, "/proc/mounts");
            while ((line = buf_reader.readLine()) != null) {
                Log.d(TAG, line);
                if (line.contains("vfat") || line.contains("/mnt")) {
                    StringTokenizer tokens = new StringTokenizer(line, " ");
                    String unused = tokens.nextToken(); //device
                    String mount_point = tokens.nextToken(); //mount point
                    if (paths.contains(mount_point)) {
                        continue;
                    }
                    unused = tokens.nextToken(); //file system
                    List<String> flags = Arrays.asList(tokens.nextToken().split(",")); //flags
                    boolean readonly = flags.contains("ro");

                    if (mount_point.equals(def_path)) {
                        paths.add(def_path);
                        list.add(0, new StorageInfo(def_path, def_path_internal, readonly, -1));
                    } else if (line.contains("/dev/block/vold")) {
                        if (!line.contains("/mnt/secure")
                            && !line.contains("/mnt/asec")
                            && !line.contains("/mnt/obb")
                            && !line.contains("/dev/mapper")
                            && !line.contains("tmpfs")) {
                            paths.add(mount_point);
                            list.add(new StorageInfo(mount_point, false, readonly, cur_display_number++));
                        }
                    }
                }
            }

            if (!paths.contains(def_path) && def_path_available) {
                list.add(0, new StorageInfo(def_path, def_path_internal, def_path_readonly, -1));
            }

        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            if (buf_reader != null) {
                try {
                    buf_reader.close();
                } catch (IOException ex) {}
            }
        }
        return list;
    }    
}

谢谢。完美地工作了。我喜欢你将StorageInfo设置为不可变的方式。另一方面,我们何必使用printStackTrace呢?我们有android.util.Log.e啊。 - Martin
1
在Nexus 5和Nexus 7上,通过OTG线连接的USB设备没有起作用。 - Khawar Raza
1
我无法使用这个来在SD卡上写文件。 - Eu Vid
与@EuVid相同的问题,在VM / AVD上工作,但在硬件上不起作用。 - spy
我的可行方案是将外部SD卡路径更改为以下内容:path = path.replace("mnt/media_rw","storage"); - cwc

11

可以通过读取标准Linux文件/proc/mounts并与vold数据 (/system/etc/vold.conf)进行交叉检查,找到任何附加SD卡的挂载位置。请注意,Environment.getExternalStorageDirectory()返回的位置可能不会出现在vold配置中(某些设备的内部存储无法卸载),但仍必须包含在列表中。但是我们没有找到一个好方法来向用户描述它们。(参考)


在我看来,使用 mount 的方法比读取 /proc 文件系统更加兼容。问题在于 SD 卡的格式不一定是 FAT。此外,卡的挂载点可能因 ROM 而异。此外,可能存在其他几个 VFAT 分区... - borisstr
1
@borisstr:嗯,实际上Android使用vold,因此查看它的配置也是适当的。 - Jan Hudec
我在上面的帖子中分享的代码文件包括一种方法,用于向用户描述发现的根路径。请查看 setProperties() 方法。 - Baron
1
@borisstr,实际上,在Android设备上读取/proc/mounts比启动mount可执行文件更具可移植性,特别是因为启动可执行文件是不鼓励的。 - Chris Stratton

8

我尝试了这个主题中的所有解决方案,但是它们都不能正确地在只有一个可移动外部卡和一个不可移动内部卡的设备上工作。从'mount'命令、'proc/mounts'文件等中获取外部卡的路径是不可能的。

因此,我创建了自己的解决方案(基于Paulo Luan的方案):

String sSDpath = null;
File   fileCur = null;
for( String sPathCur : Arrays.asList( "ext_card", "external_sd", "ext_sd", "external", "extSdCard",  "externalSdCard")) // external sdcard
{
   fileCur = new File( "/mnt/", sPathCur);
   if( fileCur.isDirectory() && fileCur.canWrite())
   {
     sSDpath = fileCur.getAbsolutePath();
     break;
   }
}
fileCur = null;
if( sSDpath == null)  sSDpath = Environment.getExternalStorageDirectory().getAbsolutePath();

6

如果您查看android.os.Environment的源代码,您会发现Android在路径上大量依赖环境变量。您可以使用“SECONDARY_STORAGE”环境变量来查找可移动SD卡的路径。

/**
 * Get a file using an environmental variable.
 *
 * @param variableName
 *         The Environment variable name.
 * @param paths
 *         Any paths to the file if the Environment variable was not found.
 * @return the File or {@code null} if the File could not be located.
 */
private static File getDirectory(String variableName, String... paths) {
    String path = System.getenv(variableName);
    if (!TextUtils.isEmpty(path)) {
        if (path.contains(":")) {
            for (String _path : path.split(":")) {
                File file = new File(_path);
                if (file.exists()) {
                    return file;
                }
            }
        } else {
            File file = new File(path);
            if (file.exists()) {
                return file;
            }
        }
    }
    if (paths != null && paths.length > 0) {
        for (String _path : paths) {
            File file = new File(_path);
            if (file.exists()) {
                return file;
            }
        }
    }
    return null;
}

使用示例:

public static final File REMOVABLE_STORAGE = getDirectory("SECONDARY_STORAGE");

5

只需简单地使用以下代码:

String primary_sd = System.getenv("EXTERNAL_STORAGE");
if(primary_sd != null)
    Log.i("EXTERNAL_STORAGE", primary_sd);
String secondary_sd = System.getenv("SECONDARY_STORAGE");
if(secondary_sd != null)
    Log.i("SECONDARY_STORAGE", secondary_sd)

在某些设备上,SECONDARY_STORAGE 有几个由冒号(“:”)分隔的路径。这就是为什么我要拆分字符串(请参见我的上面的答案)。 - Jared Rummler
这两个对我都返回 null。 - Tim Cooper

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