获取连接到Android设备的所有存储设备路径列表

16

我希望获得连接到Android设备的所有存储设备列表。

例如 - 内部存储(其中包含所有文件夹,如下载、DCIM等)、SD卡和OTG设备。

我知道有很多StackOverflow的帖子讨论了这个话题,但是没有一个能像上面所述的那样满足我的需求。

通过调用Environment.getExternalStorageDirectory().getPath()可以获取内部存储路径。

无法使用标准方法检索所有连接的存储设备列表,因此非常感谢任何帮助。

此外,许多解决方案在不同设备和Android版本上无法正常工作。

5个回答

5

您可以创建一个名为EnvironmentSDCardCheck的类。

package com.example.storagecheck;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Environment;
import android.os.storage.StorageManager;
import android.support.v4.content.ContextCompat;
import android.support.v4.os.EnvironmentCompat;
import android.util.Log;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;

public class EnvironmentSDCardCheck {
    private static final String TAG = "EnvironmentSDCardCheck";

    public final static String TYPE_PRIMARY = "primär";
    public final static String TYPE_INTERNAL = "intern";
    public final static String TYPE_SD = "MicroSD";
    public final static String TYPE_USB = "USB";
    public final static String TYPE_UNKNOWN = "unbekannt";

    public final static String WRITE_NONE = "none";
    public final static String WRITE_READONLY = "readonly";
    public final static String WRITE_APPONLY = "apponly";
    public final static String WRITE_FULL = "readwrite";

    private static Device[] devices, externalstorage, storage;
    private static BroadcastReceiver receiver;
    private static boolean useReceiver = true;
    private static String userDir;

    public static Device[] getDevices(Context context) {
        if (devices == null) initDevices(context);
        return devices;
    }

    public static Device[] getExternalStorage(Context context) {
        if (devices == null) initDevices(context);
        return externalstorage;
    }

    public static Device[] getStorage(Context context) {
        if (devices == null) initDevices(context);
        return storage;
    }

    public static IntentFilter getRescanIntentFilter() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL); 
        filter.addAction(Intent.ACTION_MEDIA_MOUNTED); 
        filter.addAction(Intent.ACTION_MEDIA_REMOVED); 
        filter.addAction(Intent.ACTION_MEDIA_SHARED); 
        filter.addDataScheme("file");
        return filter;
    }

    public static void setUseReceiver(Context context, boolean use) {
        if (use && receiver == null) {
            receiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    Log.i(TAG, "Storage " + intent.getAction() + "-" + intent.getData());
                    initDevices(context);
                }
            };
            context.registerReceiver(receiver, getRescanIntentFilter());
        } else if (!use && receiver != null) {
            context.unregisterReceiver(receiver);
            receiver = null;
        }
        useReceiver = use;
    }

    public static void initDevices(Context context) {
        if (userDir == null) userDir = "/Android/data/" + context.getPackageName();
        setUseReceiver(context, useReceiver);
        StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
        Class c = sm.getClass();
        Object[] vols;
        try {
            Method m = c.getMethod("getVolumeList", null);
            vols = (Object[]) m.invoke(sm, null); // android.os.Storage.StorageVolume
            Device[] temp = new Device[vols.length];
            for (int i = 0; i < vols.length; i++) temp[i] = new Device(vols[i]);
            Device primary = null;
            for (Device d : temp) if (d.mPrimary) primary = d;
            if (primary == null) for (Device d : temp)
                if (!d.mRemovable) {
                    d.mPrimary = true;
                    primary = d;
                    break;
                }
            if (primary == null) {
                primary = temp[0];
                primary.mPrimary = true;
            }

            File[] files = ContextCompat.getExternalFilesDirs(context, null);
            File[] caches = ContextCompat.getExternalCacheDirs(context);
            for (Device d : temp) {
                if (files != null) for (File f : files)
                    if (f != null && f.getAbsolutePath().startsWith(d.getAbsolutePath()))
                        d.mFiles = f;
                if (caches != null) for (File f : caches)
                    if (f != null && f.getAbsolutePath().startsWith(d.getAbsolutePath()))
                        d.mCache = f;
            }

            ArrayList<Device> tempDev = new ArrayList<Device>(10);
            ArrayList<Device> tempStor = new ArrayList<Device>(10);
            ArrayList<Device> tempExt = new ArrayList<Device>(10);
            for (Device d : temp) {
                tempDev.add(d);
                if (d.isAvailable()) {
                    tempExt.add(d);
                    tempStor.add(d);
                }
            }

            Device internal = new Device(context);
            tempStor.add(0, internal); // bei Storage-Alternativen immer
            if (!primary.mEmulated) tempDev.add(0, internal); // bei Devices nur wenn zusätzlich

            devices = tempDev.toArray(new Device[tempDev.size()]);
            storage = tempStor.toArray(new Device[tempStor.size()]);
            externalstorage = tempExt.toArray(new Device[tempExt.size()]);
        } catch (Exception e) {
            // Fallback auf normale Android-Funktionen
        }

    }

    public static class Device extends File {
        String mUserLabel, mUuid, mState, mWriteState, mType;
        boolean mPrimary, mRemovable, mEmulated, mAllowMassStorage;
        long mMaxFileSize;
        File mFiles, mCache;

        Device(Context context) {
            super(Environment.getDataDirectory().getAbsolutePath());
            mState = Environment.MEDIA_MOUNTED;
            mFiles = context.getFilesDir();
            mCache = context.getCacheDir();
            mType = TYPE_INTERNAL;
            mWriteState = WRITE_APPONLY;
        }

        @SuppressWarnings("NullArgumentToVariableArgMethod")
        Device(Object storage) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
            super((String) storage.getClass().getMethod("getPath", null).invoke(storage, null));
            for (Method m : storage.getClass().getMethods()) {
                if (m.getName().equals("getUserLabel") && m.getParameterTypes().length == 0 && m.getReturnType() == String.class)
                    mUserLabel = (String) m.invoke(storage, null); // ab Android 4.4
                if (m.getName().equals("getUuid") && m.getParameterTypes().length == 0 && m.getReturnType() == String.class)
                    mUuid = (String) m.invoke(storage, null); // ab Android 4.4
                if (m.getName().equals("getState") && m.getParameterTypes().length == 0 && m.getReturnType() == String.class)
                    mState = (String) m.invoke(storage, null); // ab Android 4.4
                if (m.getName().equals("isRemovable") && m.getParameterTypes().length == 0 && m.getReturnType() == boolean.class)
                    mRemovable = (Boolean) m.invoke(storage, null); // ab Android 4.0
                if (m.getName().equals("isPrimary") && m.getParameterTypes().length == 0 && m.getReturnType() == boolean.class)
                    mPrimary = (Boolean) m.invoke(storage, null); // ab Android 4.2
                if (m.getName().equals("isEmulated") && m.getParameterTypes().length == 0 && m.getReturnType() == boolean.class)
                    mEmulated = (Boolean) m.invoke(storage, null); // ab Android 4.0
                if (m.getName().equals("allowMassStorage") && m.getParameterTypes().length == 0 && m.getReturnType() == boolean.class)
                    mAllowMassStorage = (Boolean) m.invoke(storage, null); // ab Android 4.0
                if (m.getName().equals("getMaxFileSize") && m.getParameterTypes().length == 0 && m.getReturnType() == long.class)
                    mMaxFileSize = (Long) m.invoke(storage, null); // ab Android 4.0
                // getDescription (ab 4.1 mit context) liefert keine sinnvollen Werte
                // getPathFile (ab 4.2) liefert keine sinnvollen Werte
                // getMtpReserveSpace (ab 4.0) für diese Zwecke unwichtig
                // getStorageId (ab 4.0) für diese Zwecke unwichtig
            }
            if (mState == null) mState = getState();

            if (mPrimary)
                mType = TYPE_PRIMARY;
            else {
                String n = getAbsolutePath().toLowerCase();
                if (n.indexOf("sd") > 0)
                    mType = TYPE_SD;
                else if (n.indexOf("usb") > 0)
                    mType = TYPE_USB;
                else
                    mType = TYPE_UNKNOWN + " " + getAbsolutePath();
            }
        }

        public String getType() {
            return mType;
        }

        public String getAccess() {
            if (mWriteState == null) {
                try {
                    mWriteState = WRITE_NONE;
                    File[] root = listFiles();
                    if (root == null || root.length == 0)
                        throw new IOException("root empty/unreadable");
                    mWriteState = WRITE_READONLY;
                    File t = File.createTempFile("jow", null, getFilesDir());
                    //noinspection ResultOfMethodCallIgnored
                    t.delete();
                    mWriteState = WRITE_APPONLY;
                    t = File.createTempFile("jow", null, this);
                    //noinspection ResultOfMethodCallIgnored
                    t.delete();
                    mWriteState = WRITE_FULL;
                } catch (IOException ignore) {
                    Log.v(TAG, "test " + getAbsolutePath() + " ->" + mWriteState + "<- " + ignore.getMessage());
                }
            }
            return mWriteState;
        }

        public boolean isAvailable() {
            String s = getState();
            return (
                    Environment.MEDIA_MOUNTED.equals(s) ||
                            Environment.MEDIA_MOUNTED_READ_ONLY.equals(s)
            );
            // MEDIA_SHARED: als USB freigegeben; bitte Handy auf MTP umstellen
        }

        public String getState() {
            if (mRemovable || mState == null) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
                    // Android 5.0? Da gibts was neues
                    mState = Environment.getExternalStorageState(this);
                else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
                    // Android 4.4? Dann dort nachfragen
                    mState = Environment.getStorageState(this);
                else if (canRead() && getTotalSpace() > 0)
                    // lesbar und Größe vorhanden => gibt es
                    mState = Environment.MEDIA_MOUNTED;
                else if (mState == null || Environment.MEDIA_MOUNTED.equals(mState))
                    // nicht lesbar, keine Größe aber noch MOUNTED || oder ungesetzt => UNKNOWN
                    mState = EnvironmentCompat.MEDIA_UNKNOWN;
            }
            return mState;
        }

        public File getFilesDir() {
            if (mFiles == null) {
                mFiles = new File(this, userDir + "/files");
                if (!mFiles.isDirectory())
                    //noinspection ResultOfMethodCallIgnored
                    mFiles.mkdirs();
            }
            return mFiles;
        }

        public File getCacheDir() {
            if (mCache == null) {
                mCache = new File(this, userDir + "/cache");
                if (!mCache.isDirectory())
                    //noinspection ResultOfMethodCallIgnored
                    mCache.mkdirs();
            }
            return mCache;
        }

        public boolean isPrimary() {
            return mPrimary;
        }

        public boolean isRemovable() {
            return mRemovable;
        }
        public boolean isEmulated() {
            return mEmulated;
        }

        public boolean isAllowMassStorage() {
            return mAllowMassStorage;
        }

        public long getMaxFileSize() {
            return mMaxFileSize;
        }

        public String getUserLabel() {
            return mUserLabel;
        }

        public String getUuid() {
            return mUuid;
        }
    }
}

然后您可以使用它来检查SD卡、USB或未知设备是否已连接到设备上。

这样,您就可以获取连接的SD卡、USB等信息。

private boolean checkSdCardPermission() {
    boolean flag = false;
    try {
        EnvironmentSDCard.Device[] devices = EnvironmentSDCard.getExternalStorage(MainActivity.this);
        for (EnvironmentSDCard.Device d : devices) {
            if (d.getType().equals(EnvironmentSDCard.TYPE_SD) || d.getType().contains(EnvironmentSDCard.TYPE_UNKNOWN) || d.getType().contains(EnvironmentSDCard.TYPE_USB)) {
                flag = d.isAvailable();
                if (flag)
                    break;
            }
        }
    } catch (Exception e) {
    }
    return flag;
}

谢谢,我会尝试并回来。 - Rahulrr2602
这是很多反射调用... 如果设备供应商没有在“Environment”中正确注册存储磁盘,则他们也不太可能在StorageManager中注册它,因此我认为这种方法并不有效。您有哪些设备的示例,在旧版Android上实际上会有所不同吗? - user1643723
谢谢回答,你的回答完美地解决了我的问题。我会等待其他答案,如果没有答案能够解决问题,我会接受你的回答并颁发奖励。 - Rahulrr2602
好的,没问题,你也可以尝试其他的解决方案 :) - Amjad Khan
谢谢。我已经授予了奖励并接受了您的答案作为正确答案。再次感谢。 - Rahulrr2602

2

我在IT技术方面有些经验。

ContextCompat.getExternalFilesDirs

这样可以找到外部驱动器上的应用程序文件夹。我还没有找到比这更好的解决方案。

在我的使用情况中,我正在使用Environment.DIRECTORY_MOVIES。但如果需要,还有其他定义,包括通用的DIRECTORY_DOCUMENTS


谢谢你的回答。在第二个参数 ContextCompat.getExternalFilesDir(this," ? ") 的位置上,我应该传入什么? - Rahulrr2602

1
这是对@Sagar关于从/proc获取挂载点的补充。请注意使用/proc/self/mountinfo而不是/proc/mountinfo或/proc/mounts。您可以在man 5 procfs中阅读有关/proc/self/mountinfo格式的更多信息。虽然以下代码从技术上讲解析文件,但在主线程上运行是安全的(因为/proc是内存文件系统)。
private static final int SANE_SIZE_LIMIT = 200 * 1024 * 1024;

// some hashmap for mapping long values to objects
// personally I am using HPPC maps, the HashMap class is for simplicity
public final HashMap<String> mountMap = new HashMap<>();

public void parse() {
    mountMap.clear();

    CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();

    parseMounts(decoder, true);
}

private int measure(FileChannel fc) throws IOException {
    final ByteBuffer buffer = ByteBuffer.allocate(1024 * 4);

    int totalRead = 0, lastRead;

    do {
        buffer.clear();

        lastRead = fc.read(buffer);

        totalRead += lastRead;

        if (totalRead > SANE_SIZE_LIMIT) {
            throw new IOException("/proc/ file appears to be too big!!");
        }
    } while (lastRead != -1);

    fc.position(0);

    return totalRead;
}

private void parseMounts(CharsetDecoder d, boolean force) {
  File file = new File("/proc/self/mountinfo");

  int mode = ParcelFileDescriptor.MODE_READ_ONLY;

  try (ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, mode));
    FileChannel fc = new FileInputStream(pfd.getFileDescriptor()).getChannel()) {

    // Measure size of file before reading from it.
    // Virtual files in /proc/ appear to be zero-sized (because
    // their contents are dynamic), but we want to attempt
    // reading it in single read() call to avoid inconsistencies
    final int totalRead = measure(fc);

    try (FileInputStream fis = new FileInputStream(pfd.getFileDescriptor());
         Reader r = Channels.newReader(fis.getChannel(), d, totalRead);
         Scanner scanner = new Scanner(r)) {
      while (scanner.hasNextLine()) {
        scanner.nextInt();
        scanner.nextInt();

        final String[] mm = scanner.next().split(":");

        final int major = Integer.parseInt(mm[0]);
        final int minor = Integer.parseInt(mm[1]);

        final long dev_t = makedev(major, minor);

        final String source = scanner.next();

        // ignore bind-mounts for now
        if ("/".equals(source)) {
          final String location = scanner.next();

          // skip optional parts
          scanner.skip("(.+ -)");

          // type of file system (such as ext4)
          // most useful filesystems can be distinguished by type
          // but "fuse" is problematic (because most Android
          // distributions implement dynamic permissions on
          // external SD card via custom FUSE filesystem).
          // To make matters worse, that SD card FUSE filesystem is
          // often mounted in several places at once. The code below
          // will throw away duplicate mounts by placing them in
          // HashMap, keyed by uniqie filesystem type ID,
          // but you can do it more cleverly be checking whether
          // a mountpoint directory is accessible (via File#list).
          // You can throw away rest of useless filesystems (such as
          // /mnt/secure/asec) by permission checks and blacklisting
          // well-known paths.
          final String fsType = scanner.next().intern();

          final String subject = scanner.next().intern();

          String created = location + subject + fsType;

          String prev = mountMap.put(dev_t, created);

          if (prev != null) {
            created.next = prev;
          }
        }

        scanner.nextLine();
      }

      return;
    } catch (NumberFormatException | NoSuchElementException nse) {
      // oops.. either a new row type was introduced (not a big deal)
      // or the file changed below our feet (because someone has mounted
      // something). Let's retry once more
      parseMounts(d, false);
    } catch (IOException e) {
        throw new WrappedIOException(e);
    }
}

您可以在此答案中找到更多有用的信息(例如无用文件系统的常见路径)。请注意,/proc/mounts和/proc/mountinfo的格式不同,后者是在前者基础上引入的,以改进其格式而不会破坏向后兼容性。

上面的代码并不是万能的,它不能真正告诉你关于各个文件系统的具体信息,只能提供它们的路径和文件系统名称。你可以合理地推断出"vfat"和"ext4"是有用的文件系统,而"procfs"是无用的,但类似"fuse"这样的文件系统仍然会保持神秘。你可以通过使用android.os.storage.StorageManager来增强上面代码的输出,以获取更加用户友好的文件系统名称(例如"SD卡"),当它们可用时(通过匹配挂载路径)。你还可以使用StatFs来获取分区上的可用空闲空间 - 无用的虚拟文件系统通常在查询时会返回零可用空间和零自由空间。最后,如果你愿意,你可以考虑文件系统挂载选项,在决定是否向用户显示文件系统时进行选择。例如,rorw——只读文件系统挂载通常对用户来说将会没有太多用处。
当我向人们解释这种方法时,他们经常担心它的健壮性...它能在一些随机的旧手机上工作吗?它会在将来的操作系统版本中保持可用吗?这是我的看法:即使在最坏的情况下,从/proc/文件中读取将返回IOException,但这种方法仍然比许多基于反射的建议更好。它不会使您的应用程序崩溃或导致像某些基于反射的黑客那样的不可预测行为。
/proc文件系统是由Linux内核开发人员维护的官方Linux API。通过指定不同的内核构建选项来删除它是不可能的(例如,它是操作系统内核的强制部分)。它已经存在多年,并且保留了比大多数Android API更好的向后兼容性。特别是/proc/self/mountinfo创建于10多年前,将在除最古老版本之外的大多数现有Android版本中提供。
Android开发人员没有正式支持Linux特定的API。但他们也不会刻意破坏它们。后-Lollipop Android中的一些最近的SELinux更改限制了对/proc/中某些文件的访问,因为它们允许应用程序秘密监视其他应用程序。这些更改特别保持了/proc/self的可访问性,因为/proc/self旨在仅公开应用程序自己的信息(包括有关文件系统的信息,可供应用程序使用)。
如果Google从Linux转向Fuchensa或其他自主开发的BSD分支,/proc/和其他Linux特定API可能会中断。我在意吗?不是很在意。

尝试了您的方法,但在Android Studio中出现了一些错误。一些错误包括找不到Mount类,throwawayBuffer.get()显示为错误,pfd.getFileDescriptor()显示为错误,scanner.hasNextLine()也显示为错误等等。您能否请看一下并提供帮助? - Rahulrr2602
@Rahulrr2602 我已经删除了对 throwawayBufferMount 的引用,它们只是一些 ByteBuffer 和简单的 POJO 用于存储挂载点信息。这并不是一个完整的可直接使用的库——我希望你能够根据自己的环境来适应代码。 - user1643723

1
自 API 级别 9 起,有 android.os.storage.StorageManager。调用 getStorageVolumes()(自 API 级别 24 起可用)以获取存储卷列表。正如文档所述:

返回当前用户可用的共享/外部存储卷列表。这包括主共享存储设备和任何连接的外部卷,包括 SD 卡和 USB 驱动器。

结果是 List<StorageVolume>。现在,请查看 android.os.storage.StorageVolume

有关特定用户的共享/外部存储卷的信息。

例如,您可以通过调用 getDescription() 获取卷的用户可见描述。请参阅 createAccessIntent() 以了解如何获取访问权限。

这个方法是否公开适用于旧版的Android系统? - Rahulrr2602
API level 9 意味着 Android 2.3(姜饼),于2010年12月6日发布(已经超过七年了)。 - user5956451
1
StorageManager在API 9中被引入,但是getStorageVolumes直到API 24才成为公共API。此外,它不会列出任何设备供应商没有明确列出的内容。换句话说,如果您的设备在公共API中没有列出某些存储位置,那么您就回到了起点。 - user1643723

0
这个函数确保在每个Android版本上获取所有规范的可读存储路径(希望如此)。
private static ArrayList<String> getAllStorages(Context context)
{
    ArrayList<String> storagePaths = new ArrayList<>();
    
    storagePaths.add(Environment.getExternalStorageDirectory().getAbsolutePath());

    // Android 10+
    File[] externalDirs = context.getExternalFilesDirs(null);
    for (File file : externalDirs)
    {
        storagePaths.add(file.getAbsolutePath());
    }

    // Android 9-
    String externalStorage = System.getenv("SECONDARY_STORAGE");
    if (externalStorage != null && !externalStorage.isEmpty())
    {
        String[] externalPaths = externalStorage.split(":");
        for (String e:externalPaths)
        {
            storagePaths.add(e);
        }
    }
    externalStorage = System.getenv("EXTERNAL_STORAGE");
    if (externalStorage != null && !externalStorage.isEmpty())
    {
        String[] externalPaths = externalStorage.split(":");
        for (String e:externalPaths)
        {
            storagePaths.add(e);
        }
    }
    
    ArrayList<String> result=new ArrayList<>();
    for (String s:storagePaths)
    {
        File f=new File(s);
        File f2=f;
        while (f2 != null)
        {
            if (f2.canRead())
            {
                f = f2;
            }
            f2 = f2.getParentFile();
        }
        try
        {
            f = f.getCanonicalFile();
        }
        catch (IOException e)
        {}
        s = f.getPath();
        if (!result.contains(s))
        {
            result.add(s);
        }
    }
    return result;
}

在Android 11上进行了测试。

您可以在每个Android设备上进行测试,并在评论中提供反馈,这样我们可以使其更加兼容(如果不兼容的话)。


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