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

获取存储卡路径的接口大家都很熟悉,一般是通过 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


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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,388评论 25 707
  • 要善于给困境中的自己画饼,我说真的。 最近的事儿真多,组织处于换届的时期,各种文件要准备,写稿子、彩排等,当然其中...
    Firewinter阅读 642评论 2 0
  • 古人读书必先净手焚香,沐浴更衣,高堂内整襟危坐,何其严肃认真,实在是令我辈敬佩。而我们现在很多家庭的书刊杂志...
    7fbc83bd54dd阅读 324评论 0 0
  • 以下心得都是个人通过学习后实践验证,或者直接从实践中总结而来,视野有限难免偏颇,还请大家多多交流~本文大约 170...
    培训经理陈维磊阅读 894评论 2 3