美文网首页Android知识
Android 准确获取外置存储卡路径的方法

Android 准确获取外置存储卡路径的方法

作者: _Lyux | 来源:发表于2017-11-30 15:22 被阅读0次

    获取存储卡路径的接口大家都很熟悉,一般是通过 Environment 接口来获取:

    File sdcardRoot = Environment.getExternalStorageDirectory();
    

    偶尔开发中会遇到需要获取外置存储卡的接口,一般是 TF小卡,网上有很多方法,但都不是完全准确的方法.
    下面提供一个准确获取外置存储卡路径的方法.

    原理:

    Android 4.0 版本中谷歌其实已经加入了对多张存储卡的支持,而且支持代码相当完善.但其获取多存储卡路径和状态的三个接口却标注了@hide,所以在Android标准SDK中没有这三个方法.

    这些接口在Android源代码中位置如下:

    frameworks/base/core/java/android/os/storage/StorageManager.java
    

    StorageManager类中,以下三个接口都被标注为@hide:

    getVolumePaths()
    返回全部存储卡路径, 包括已挂载的和未挂载的.
    即: 有外置存储卡卡槽的机器,即使未插入外置存储卡,其路径也会被这个接口列出.
    要判断某个挂载点的状态,可以用第三个接口.

    getVolumeList()
    返回全部 StorageVolume 类的数组,这个类也是 @hide 的.
    该类提供了更详细的关于每个挂载点的信息.具体有什么信息,请继续向下看.

    getVolumeState(String mountPoint)
    返回某个挂载点代表的存储卡的状态. 即 Environment 中的几个常量(未全部列出):

    Environment.MEDIA_REMOVED
    Environment.MEDIA_MOUNTED
    

    上面第一个和第二个方法都返回数组,都是数组第一个为主存储卡,第二个是副存储卡(如果该机型支持的话).
    一般主存储卡就是手机内建的存储卡.不过也有厂商自己开发了存储卡切换功能,这时候主存储卡可能会被设为外置存储卡.这个不展开.
    如果手机支持插入外置卡,那么无论有无TF卡插入卡槽,上面返回的数组长度是固定的.没有TF卡插入时,其状态即为 Environment.MEDIA_REMOVED.
    所以要操作外置存储卡前,要先检查其状态是否在 Environment.MEDIA_MOUNTEDEnvironment.MEDIA_MOUNTED_READ_ONLY 的状态.
    另1: 当然数组中也有可能不只两个元素.比如很多手机都支持OTG功能,就是用USB线连接U盘到手机的功能,这时上面方法返回的数组就会有三个元素,一般第三个元素就是 USB OTG.和外置存储卡一样,是否有U盘插入, OTG 都会在数组里被返回的.
    另2: 一般能上市的4.0以上的手机,上面那几个接口肯定是有的.没有的话,就过不了谷歌的兼容性测试(CTS),就上不了市. 而且一般也没谁蛋疼会去改这几个接口.

    那么问题来了, SDK 访问不到的方法,知道又有何用呢.
    嗯,小明同学说的对,可以用反射.

    简单的获取外置存储卡路径的方法:

    import java.lang.reflect.Method;
    import android.os.storage.StorageManager;
    
        // 获取主存储卡路径
        public String getPrimaryStoragePath() {
            try {
                StorageManager sm = (StorageManager) getSystemService(STORAGE_SERVICE);
                Method getVolumePathsMethod = StorageManager.class.getMethod("getVolumePaths", null);
                String[] paths = (String[]) getVolumePathsMethod.invoke(sm, null);
                // first element in paths[] is primary storage path
                return paths[0];
            } catch (Exception e) {
                Log.e(TAG, "getPrimaryStoragePath() failed", e);
            }
            return null;
        }
        
        // 获取次存储卡路径,一般就是外置 TF 卡了. 不过也有可能是 USB OTG 设备...
        // 其实只要判断第二章卡在挂载状态,就可以用了.
        public String getSecondaryStoragePath() {
            try {
                StorageManager sm = (StorageManager) getSystemService(STORAGE_SERVICE);
                Method getVolumePathsMethod = StorageManager.class.getMethod("getVolumePaths", null);
                String[] paths = (String[]) getVolumePathsMethod.invoke(sm, null);
                // second element in paths[] is secondary storage path
                return paths.length <= 1 ? null : paths[1];
            } catch (Exception e) {
                Log.e(TAG, "getSecondaryStoragePath() failed", e);
            }
            return null;
        }
        
        // 获取存储卡的挂载状态. path 参数传入上两个方法得到的路径
        public String getStorageState(String path) {
            try {
                StorageManager sm = (StorageManager) getSystemService(STORAGE_SERVICE);
                Method getVolumeStateMethod = StorageManager.class.getMethod("getVolumeState", new Class[] {String.class});
                String state = (String) getVolumeStateMethod.invoke(sm, path);
                return state;
            } catch (Exception e) {
                Log.e(TAG, "getStorageState() failed", e);
            }
            return null;
        }
    

    获取关于存储卡的更详细的信息:

    在原理那节提到了 StorageVolume 可以提供更详细的存储卡信息.这里当然还是要用反射.
    为了便于调用,写了一个基于反射的代理类,将 StorageVolume 信息全部暴露出来. 这些信息包括:

    isPrimary() - 是否主存储卡
    isRemovable() - 是否可移除. 内建存储卡肯定返回 false, 外置TF卡肯定返回 true
    isEmulated() - 4.0 谷歌采用 fuse 文件系统后, 多数 Android 机的内建存储卡其实就都是虚拟的了.不过同学们知道这信息也没什么用处.
    allowMassStorage() - 是否支持传统的大容量存储模式,就是早期那个黑底绿机器人举个USB的那个界面. 如上,用 fuse 的手机很少有支持这个功能的. 现在都用 MTP 了.
    getMaxFileSize() - 获取磁盘最大可用容量.注意这个不一定就和磁盘容量完全相等,有的厂商会预留比如50MB出来作为最后阈值: 存储卡真全满了后果还是很严重的.
    getMtpReserveSpace() - 同上, MTP 模式下的最大可用容量.这个一般厂商好像都会预留一些空间出来,防止用户用 MTP 填满磁盘.

    此类没有太多可说的,就是反射反射再反射而已.不再一一解释了,直接贴代码:

    package com.lx.mystalecode.utils;
    
    import android.content.Context;
    import android.os.storage.StorageManager;
    
    import java.io.File;
    import java.lang.reflect.Array;
    import java.lang.reflect.Method;
    
    /**
     *
     * author: liuxu
     * date: 2014-10-27
     *
     * There are some useful methods in StorageManager, like:
     * StorageManager.getVolumeList()
     * StorageManager.getVolumeState()
     * StorageManager.getVolumePaths()
     * But for now these methods are not visible in SDK (marked as \@hide).
     * one requirement for these methods is to get secondary storage or
     * OTG disk info.
     *
     * here we use java reflect mechanism to retrieve these methods and data.
     *
     * Demo: ActivityStorageUtilsDemo
     */
    public final class StorageManagerHack {
    
        private StorageManagerHack() {
        }
    
        public static StorageManager getStorageManager(Context cxt) {
            StorageManager sm = (StorageManager)
                    cxt.getSystemService(Context.STORAGE_SERVICE);
            return sm;
        }
    
        /**
         * Returns list of all mountable volumes.
         * list elements are RefStorageVolume, which can be seen as
         * mirror of android.os.storage.StorageVolume
         * return null on error.
         * @param cxt
         * @return
         */
        public static RefStorageVolume[] getVolumeList(Context cxt) {
            if (!isSupportApi()) {
                return null;
            }
            StorageManager sm = getStorageManager(cxt);
            if (sm == null) {
                return null;
            }
    
            try {
                Class<?>[] argTypes = new Class[0];
                Method method_getVolumeList =
                        StorageManager.class.getMethod("getVolumeList", argTypes);
                Object[] args = new Object[0];
                Object array = method_getVolumeList.invoke(sm, args);
                int arrLength = Array.getLength(array);
                RefStorageVolume[] volumes = new
                        RefStorageVolume[arrLength];
                for (int i = 0; i < arrLength; i++) {
                    volumes[i] = new RefStorageVolume(Array.get(array, i));
                }
                return volumes;
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * Returns list of paths for all mountable volumes.
         * return null on error.
         */
        public static String[] getVolumePaths(Context cxt) {
            if (!isSupportApi()) {
                return null;
            }
            StorageManager sm = getStorageManager(cxt);
            if (sm == null) {
                return null;
            }
    
            try {
                Class<?>[] argTypes = new Class[0];
                Method method_getVolumeList =
                        StorageManager.class.getMethod("getVolumePaths", argTypes);
                Object[] args = new Object[0];
                Object array = method_getVolumeList.invoke(sm, args);
                int arrLength = Array.getLength(array);
                String[] paths = new
                        String[arrLength];
                for (int i = 0; i < arrLength; i++) {
                    paths[i] = (String) Array.get(array, i);
                }
                return paths;
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * Gets the state of a volume via its mountpoint.
         * return null on error.
         */
        public static String getVolumeState(Context cxt, String mountPoint) {
            if (!isSupportApi()) {
                return null;
            }
            StorageManager sm = getStorageManager(cxt);
            if (sm == null) {
                return null;
            }
    
            try {
                Class<?>[] argTypes = new Class[1];
                argTypes[0] = String.class;
                Method method_getVolumeList =
                        StorageManager.class.getMethod("getVolumeState", argTypes);
                Object[] args = new Object[1];
                args[0] = mountPoint;
                Object obj = method_getVolumeList.invoke(sm, args);
                String state = (String) obj;
                return state;
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * Get primary volume of the device.
         * @param cxt
         * @return RefStorageVolume can be seen as mirror of
         *         android.os.storage.StorageVolume
         */
        public static RefStorageVolume getPrimaryVolume(Context cxt) {
            RefStorageVolume[] volumes = getVolumeList(cxt);
            if (volumes == null) {
                return null;
            }
            for (RefStorageVolume volume : volumes) {
                try {
                    if (volume.isPrimary()) {
                        return volume;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    return null;
                }
            }
            return null;
        }
    
        /**
         * see if SDK version of current device is greater
         * than 14 (IceCreamSandwich, 4.0).
         */
        private static boolean isSupportApi() {
            int osVersion = android.os.Build.VERSION.SDK_INT;
            boolean avail = osVersion >= 14;
            return avail;
        }
    
        /**
         * this class can be seen as mirror of android.os.storage.StorageVolume :
         * Description of a storage volume and its capabilities, including the
         * filesystem path where it may be mounted.
         */
        public static class RefStorageVolume {
    
            private static final int INIT_FLAG_STORAGE_ID = 0x01 << 0;
            private static final int INIT_FLAG_DESCRIPTION_ID = 0x01 << 1;
            private static final int INIT_FLAG_PATH = 0x01 << 2;
            private static final int INIT_FLAG_PRIMARY = 0x01 << 3;
            private static final int INIT_FLAG_REMOVABLE = 0x01 << 4;
            private static final int INIT_FLAG_EMULATED = 0x01 << 5;
            private static final int INIT_FLAG_ALLOW_MASS_STORAGE = 0x01 << 6;
            private static final int INIT_FLAG_MTP_RESERVE_SPACE = 0x01 << 7;
            private static final int INIT_FLAG_MAX_FILE_SIZE = 0x01 << 8;
            private int mInitFlags = 0x00;
    
            private int mStorageId;
            private int mDescriptionId;
            private File mPath;
            private boolean mPrimary;
            private boolean mRemovable;
            private boolean mEmulated;
            private boolean mAllowMassStorage;
            private int mMtpReserveSpace;
            /** Maximum file size for the storage, or zero for no limit */
            private long mMaxFileSize;
    
            private Class<?> class_StorageVolume =
                    Class.forName("android.os.storage.StorageVolume");
            private Object instance;
    
            private RefStorageVolume(Object obj) throws ClassNotFoundException {
                if (!class_StorageVolume.isInstance(obj)) {
                    throw new IllegalArgumentException(
                            "obj not instance of StorageVolume");
                }
                instance = obj;
            }
    
            public void initAllFields() throws Exception {
                getPathFile();
                getDescriptionId();
                getStorageId();
                isPrimary();
                isRemovable();
                isEmulated();
                allowMassStorage();
                getMaxFileSize();
                getMtpReserveSpace();
            }
    
            /**
             * Returns the mount path for the volume.
             * @return the mount path
             * @throws Exception
             */
            public String getPath() throws Exception {
                File pathFile = getPathFile();
                if (pathFile != null) {
                    return pathFile.toString();
                } else {
                    return null;
                }
            }
    
            public File getPathFile() throws Exception {
                if ((mInitFlags & INIT_FLAG_PATH) == 0) {
                    Class<?>[] argTypes = new Class[0];
                    Method method = class_StorageVolume.getDeclaredMethod(
                            "getPathFile", argTypes);
                    Object[] args = new Object[0];
                    Object obj = method.invoke(instance, args);
                    mPath = (File) obj;
                    mInitFlags &= INIT_FLAG_PATH;
                }
                return mPath;
            }
    
            /**
             * Returns a user visible description of the volume.
             * @return the volume description
             * @throws Exception
             */
            public String getDescription(Context context) throws Exception {
                int resId = getDescriptionId();
                if (resId != 0) {
                    return context.getResources().getString(resId);
                } else {
                    return null;
                }
            }
    
            public int getDescriptionId() throws Exception {
                if ((mInitFlags & INIT_FLAG_DESCRIPTION_ID) == 0) {
                    Class<?>[] argTypes = new Class[0];
                    Method method = class_StorageVolume.getDeclaredMethod(
                            "getDescriptionId", argTypes);
                    Object[] args = new Object[0];
                    Object obj = method.invoke(instance, args);
                    mDescriptionId = (Integer) obj;
                    mInitFlags &= INIT_FLAG_DESCRIPTION_ID;
                }
                return mDescriptionId;
            }
    
            public boolean isPrimary() throws Exception {
                if ((mInitFlags & INIT_FLAG_PRIMARY) == 0) {
                    Class<?>[] argTypes = new Class[0];
                    Method method = class_StorageVolume.getDeclaredMethod(
                            "isPrimary", argTypes);
                    Object[] args = new Object[0];
                    Object obj = method.invoke(instance, args);
                    mPrimary = (Boolean) obj;
                    mInitFlags &= INIT_FLAG_PRIMARY;
                }
                return mPrimary;
            }
    
            /**
             * Returns true if the volume is removable.
             * @return is removable
             */
            public boolean isRemovable() throws Exception {
                if ((mInitFlags & INIT_FLAG_REMOVABLE) == 0) {
                    Class<?>[] argTypes = new Class[0];
                    Method method = class_StorageVolume.getDeclaredMethod(
                            "isRemovable", argTypes);
                    Object[] args = new Object[0];
                    Object obj = method.invoke(instance, args);
                    mRemovable = (Boolean) obj;
                    mInitFlags &= INIT_FLAG_REMOVABLE;
                }
                return mRemovable;
            }
    
            /**
             * Returns true if the volume is emulated.
             * @return is removable
             */
            public boolean isEmulated() throws Exception {
                if ((mInitFlags & INIT_FLAG_EMULATED) == 0) {
                    Class<?>[] argTypes = new Class[0];
                    Method method = class_StorageVolume.getDeclaredMethod(
                            "isEmulated", argTypes);
                    Object[] args = new Object[0];
                    Object obj = method.invoke(instance, args);
                    mEmulated = (Boolean) obj;
                    mInitFlags &= INIT_FLAG_EMULATED;
                }
                return mEmulated;
            }
    
            /**
             * Returns the MTP storage ID for the volume.
             * this is also used for the storage_id column in the media provider.
             * @return MTP storage ID
             */
            public int getStorageId() throws Exception {
                if ((mInitFlags & INIT_FLAG_STORAGE_ID) == 0) {
                    Class<?>[] argTypes = new Class[0];
                    Method method = class_StorageVolume.getDeclaredMethod(
                            "getStorageId", argTypes);
                    Object[] args = new Object[0];
                    Object obj = method.invoke(instance, args);
                    mStorageId = (Integer) obj;
                    mInitFlags &= INIT_FLAG_STORAGE_ID;
                }
                return mStorageId;
            }
    
            /**
             * Returns true if this volume can be shared via USB mass storage.
             * @return whether mass storage is allowed
             */
            public boolean allowMassStorage() throws Exception {
                if ((mInitFlags & INIT_FLAG_ALLOW_MASS_STORAGE) == 0) {
                    Class<?>[] argTypes = new Class[0];
                    Method method = class_StorageVolume.getDeclaredMethod(
                            "allowMassStorage", argTypes);
                    Object[] args = new Object[0];
                    Object obj = method.invoke(instance, args);
                    mAllowMassStorage = (Boolean) obj;
                    mInitFlags &= INIT_FLAG_ALLOW_MASS_STORAGE;
                }
                return mAllowMassStorage;
            }
    
            /**
             * Returns maximum file size for the volume, or zero if it is unbounded.
             * @return maximum file size
             */
            public long getMaxFileSize() throws Exception {
                if ((mInitFlags & INIT_FLAG_MAX_FILE_SIZE) == 0) {
                    Class<?>[] argTypes = new Class[0];
                    Method method = class_StorageVolume.getDeclaredMethod(
                            "getMaxFileSize", argTypes);
                    Object[] args = new Object[0];
                    Object obj = method.invoke(instance, args);
                    mMaxFileSize = (Long) obj;
                    mInitFlags &= INIT_FLAG_MAX_FILE_SIZE;
                }
                return mMaxFileSize;
            }
    
            /**
             * Number of megabytes of space to leave unallocated by MTP.
             * MTP will subtract this value from the free space it reports back
             * to the host via GetStorageInfo, and will not allow new files to
             * be added via MTP if there is less than this amount left free in the
             * storage.
             * If MTP has dedicated storage this value should be zero, but if MTP is
             * sharing storage with the rest of the system, set this to a positive
             * value
             * to ensure that MTP activity does not result in the storage being
             * too close to full.
             * @return MTP reserve space
             */
            public int getMtpReserveSpace() throws Exception {
                if ((mInitFlags & INIT_FLAG_MTP_RESERVE_SPACE) == 0) {
                    Class<?>[] argTypes = new Class[0];
                    Method method = class_StorageVolume.getDeclaredMethod(
                            "getMtpReserveSpace", argTypes);
                    Object[] args = new Object[0];
                    Object obj = method.invoke(instance, args);
                    mMtpReserveSpace = (Integer) obj;
                    mInitFlags &= INIT_FLAG_MTP_RESERVE_SPACE;
                }
                return mMtpReserveSpace;
            }
    
            @Override
            public String toString() {
                try {
                    final StringBuilder builder = new StringBuilder("RefStorageVolume [");
                    builder.append("mStorageId=").append(getStorageId());
                    builder.append(" mPath=").append(getPath());
                    builder.append(" mDescriptionId=").append(getDescriptionId());
                    builder.append(" mPrimary=").append(isPrimary());
                    builder.append(" mRemovable=").append(isRemovable());
                    builder.append(" mEmulated=").append(isEmulated());
                    builder.append(" mMtpReserveSpace=").append(getMtpReserveSpace());
                    builder.append(" mAllowMassStorage=").append(allowMassStorage());
                    builder.append(" mMaxFileSize=").append(getMaxFileSize());
                    builder.append("]");
                    return builder.toString();
                } catch (Exception e) {
                    e.printStackTrace();
                    return null;
                }
            }
        }
    
    }
    

    上面这个类收录在作者的 github 内:
    https://github.com/liuxu0703/MyAndroidStaleCode/blob/master/app/src/main/java/com/lx/mystalecode/utils/StorageManagerHack.java


    最后我想说,然并卵...
    并没有好的方法获取外置存储卡完全的读写权限,即使获取了外置存储卡路径,也得按谷歌的规则玩,才能有限的操作外置存储卡.
    所以我猜测,想拿到外置存储卡路径的各位同学,需求方面应该也是比较奇葩的吧...
    这个悲伤的故事在这里就不展开了.

    相关文章

      网友评论

        本文标题:Android 准确获取外置存储卡路径的方法

        本文链接:https://www.haomeiwen.com/subject/zlvvbxtx.html