獲取儲存卡路徑的介面大家都很熟悉,一般是通過 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_MOUNTED 或 Environment.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()

// 是否可移除. 內建儲存卡肯定返回 false, 外接TF卡肯定返回 true

isRemovable()

// 4.0 谷歌採用 fuse 檔案系統後, 多數 Android 機的內建儲存卡其實就都是虛擬的了.不過同學們知道這資訊也沒什麼用處.

isEmulated()

// 是否支援傳統的大容量儲存模式,就是早期那個黑底綠機器人舉個USB的那個介面. 如上,用 fuse 的手機很少有支援這個功能的. 現在都用 MTP 了.

allowMassStorage()

// 獲取磁碟最大可用容量.注意這個不一定就和磁碟容量完全相等,有的廠商會預留比如50MB出來作為最後閾值: 儲存卡真全滿了後果還是很嚴重的.

getMaxFileSize()

// 同上, MTP 模式下的最大可用容量.這個一般廠商好像都會預留一些空間出來,防止使用者用 MTP 填滿磁碟.

getMtpReserveSpace()

此類沒有太多可說的,就是反射反射再反射而已.不再一一解釋了,直接貼程式碼:

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

最後我想說,然並卵...

並沒有好的方法獲取外接儲存卡完全的讀寫許可權,即使獲取了外接儲存卡路徑,也得按谷歌的規則玩,才能有限的操作外接儲存卡.

所以我猜測,想拿到外接儲存卡路徑的各位同學,需求方面應該也是比較奇葩的吧...

這個悲傷的故事在這裡就不展開了.