有没有一种通用的方法来找到外置SD卡的位置?
请不要与外部存储混淆。
Environment.getExternalStorageState()返回内部SD挂载点的路径,例如/mnt/sdcard。但问题是关于外部SD的。我如何获得类似/mnt/sdcard/external_sd的路径(它可能因设备而异)?
我想我将以根据文件系统名称过滤mount命令的输出作为结束。但我不确定这种方式是否足够稳健。
有没有一种通用的方法来找到外置SD卡的位置?
请不要与外部存储混淆。
Environment.getExternalStorageState()返回内部SD挂载点的路径,例如/mnt/sdcard。但问题是关于外部SD的。我如何获得类似/mnt/sdcard/external_sd的路径(它可能因设备而异)?
我想我将以根据文件系统名称过滤mount命令的输出作为结束。但我不确定这种方式是否足够稳健。
当前回答
有没有一种通用的方法来找到外置SD卡的位置?
通用方式,如果你指的是官方方式;是的,有一个。
在API级别19中,即在Android版本4.4 Kitkat中,他们在上下文类中添加了File[] getExternalFilesDirs(字符串类型),允许应用程序在micro SD卡中存储数据/文件。
Android 4.4是第一个允许应用程序使用SD卡存储的平台。API级别19之前对SD卡的任何访问都是通过私有的、不受支持的API。
getExternalFilesDirs(String类型)返回所有共享/外部存储设备上特定于应用程序目录的绝对路径。这意味着,它将返回内部和外部内存的路径。通常,第二个返回路径将是microSD卡的存储路径(如果有的话)。
但请注意,
共享存储可能并不总是可用的,因为可移动媒体可以 被用户弹出。媒体状态可以检查使用 getExternalStorageState(文件)。 这些文件没有安全措施。例如,任何 持有WRITE_EXTERNAL_STORAGE的应用程序可以写入这些文件。
根据谷歌/官方Android文档,内部和外部存储术语与我们想象的大不相同。
其他回答
由于上面我最初的回答,扫描vold在各个制造商中不再可行。
我发明了一种更可靠更直接的方法。
File mnt = new File("/storage");
if (!mnt.exists())
mnt = new File("/mnt");
File[] roots = mnt.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.isDirectory() && pathname.exists()
&& pathname.canWrite() && !pathname.isHidden()
&& !isSymlink(pathname);
}
});
根目录将包含系统上所有可写的根目录,包括任何usb连接的usb设备。
注意:canWrite方法需要android.permission。WRITE_EXTERNAL_STORAGE许可。
我有一个使用ListPreference的应用程序,其中要求用户选择他们想要保存东西的位置。
在那个应用程序中,我扫描了/proc/mounts和/system/etc/vold。Fstab用于sdcard挂载点。我将每个文件的挂载点存储到两个单独的arraylist中。
然后,我将一个列表与另一个列表进行比较,并丢弃了不在两个列表中的项目。这给了我一个到每个sdcard的根路径列表。
从那里,我用File.exists()、File.isDirectory()和File.canWrite()测试了这些路径。如果这些测试中的任何一个为假,我将从列表中丢弃该路径。
无论列表中剩下什么,我都转换为String[]数组,以便ListPreference值属性可以使用它。
您可以在这里查看代码
我已经创建了一个utils方法来检查SD卡在设备上是否可用,并获得设备上的SD卡路径(如果可用)。
你可以复制2方法波纹到你的项目的类,你需要。这是所有。
public String isRemovableSDCardAvailable() {
final String FLAG = "mnt";
final String SECONDARY_STORAGE = System.getenv("SECONDARY_STORAGE");
final String EXTERNAL_STORAGE_DOCOMO = System.getenv("EXTERNAL_STORAGE_DOCOMO");
final String EXTERNAL_SDCARD_STORAGE = System.getenv("EXTERNAL_SDCARD_STORAGE");
final String EXTERNAL_SD_STORAGE = System.getenv("EXTERNAL_SD_STORAGE");
final String EXTERNAL_STORAGE = System.getenv("EXTERNAL_STORAGE");
Map<Integer, String> listEnvironmentVariableStoreSDCardRootDirectory = new HashMap<Integer, String>();
listEnvironmentVariableStoreSDCardRootDirectory.put(0, SECONDARY_STORAGE);
listEnvironmentVariableStoreSDCardRootDirectory.put(1, EXTERNAL_STORAGE_DOCOMO);
listEnvironmentVariableStoreSDCardRootDirectory.put(2, EXTERNAL_SDCARD_STORAGE);
listEnvironmentVariableStoreSDCardRootDirectory.put(3, EXTERNAL_SD_STORAGE);
listEnvironmentVariableStoreSDCardRootDirectory.put(4, EXTERNAL_STORAGE);
File externalStorageList[] = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
externalStorageList = getContext().getExternalFilesDirs(null);
}
String directory = null;
int size = listEnvironmentVariableStoreSDCardRootDirectory.size();
for (int i = 0; i < size; i++) {
if (externalStorageList != null && externalStorageList.length > 1 && externalStorageList[1] != null)
directory = externalStorageList[1].getAbsolutePath();
else
directory = listEnvironmentVariableStoreSDCardRootDirectory.get(i);
directory = canCreateFile(directory);
if (directory != null && directory.length() != 0) {
if (i == size - 1) {
if (directory.contains(FLAG)) {
Log.e(getClass().getSimpleName(), "SD Card's directory: " + directory);
return directory;
} else {
return null;
}
}
Log.e(getClass().getSimpleName(), "SD Card's directory: " + directory);
return directory;
}
}
return null;
}
/**
* Check if can create file on given directory. Use this enclose with method
* {@link BeginScreenFragement#isRemovableSDCardAvailable()} to check sd
* card is available on device or not.
*
* @param directory
* @return
*/
public String canCreateFile(String directory) {
final String FILE_DIR = directory + File.separator + "hoang.txt";
File tempFlie = null;
try {
tempFlie = new File(FILE_DIR);
FileOutputStream fos = new FileOutputStream(tempFlie);
fos.write(new byte[1024]);
fos.flush();
fos.close();
Log.e(getClass().getSimpleName(), "Can write file on this directory: " + FILE_DIR);
} catch (Exception e) {
Log.e(getClass().getSimpleName(), "Write file error: " + e.getMessage());
return null;
} finally {
if (tempFlie != null && tempFlie.exists() && tempFlie.isFile()) {
// tempFlie.delete();
tempFlie = null;
}
}
return directory;
}
谷歌已经阻止了许多选项,以获得外部sd卡路径,并添加权限级别,以保护sd卡目录从应用程序的垃圾。 我尝试过的每个解决方案都没有提供足够的方法来获得外部和可移动sd卡路径。
在那之后, 谷歌确实提供了一种方法来获取外部路径,应用程序可以写入它,并检查它是否可移动。
使用这个简单的API,您可以获得可移动外部目录的路径,并具有正确的写/读权限。
File[] files = getExternalFilesDirs(null);
for(File file : files){
if(Environment.isExternalStorageRemovable(file)){
return file;
}
}
下面是我用来寻找可移动SD卡的方法。它很复杂,在某些情况下可能有些过度,但它适用于我过去几年测试过的各种Android版本和设备制造商。自从API级别15以来,我不知道任何设备上没有找到SD卡,如果有一个挂载的。在大多数情况下,它不会返回假阳性,特别是如果您给它一个已知文件的名称来查找。
请让我知道如果你遇到任何情况下,它不起作用。
import android.content.Context;
import android.os.Build;
import android.os.Environment;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.regex.Pattern;
public class SDCard {
private static final String TAG = "SDCard";
/** In some scenarios we can expect to find a specified file or folder on SD cards designed
* to work with this app. If so, set KNOWNFILE to that filename. It will make our job easier.
* Set it to null otherwise. */
private static final String KNOWNFILE = null;
/** Common paths for microSD card. **/
private static String[] commonPaths = {
// Some of these taken from
// https://stackoverflow.com/questions/13976982/removable-storage-external-sdcard-path-by-manufacturers
// These are roughly in order such that the earlier ones, if they exist, are more sure
// to be removable storage than the later ones.
"/mnt/Removable/MicroSD",
"/storage/removable/sdcard1", // !< Sony Xperia Z1
"/Removable/MicroSD", // Asus ZenPad C
"/removable/microsd",
"/external_sd", // Samsung
"/_ExternalSD", // some LGs
"/storage/extSdCard", // later Samsung
"/storage/extsdcard", // Main filesystem is case-sensitive; FAT isn't.
"/mnt/extsd", // some Chinese tablets, e.g. Zeki
"/storage/sdcard1", // If this exists it's more likely than sdcard0 to be removable.
"/mnt/extSdCard",
"/mnt/sdcard/external_sd",
"/mnt/external_sd",
"/storage/external_SD",
"/storage/ext_sd", // HTC One Max
"/mnt/sdcard/_ExternalSD",
"/mnt/sdcard-ext",
"/sdcard2", // HTC One M8s
"/sdcard1", // Sony Xperia Z
"/mnt/media_rw/sdcard1", // 4.4.2 on CyanogenMod S3
"/mnt/sdcard", // This can be built-in storage (non-removable).
"/sdcard",
"/storage/sdcard0",
"/emmc",
"/mnt/emmc",
"/sdcard/sd",
"/mnt/sdcard/bpemmctest",
"/mnt/external1",
"/data/sdext4",
"/data/sdext3",
"/data/sdext2",
"/data/sdext",
"/storage/microsd" //ASUS ZenFone 2
// If we ever decide to support USB OTG storage, the following paths could be helpful:
// An LG Nexus 5 apparently uses usb://1002/UsbStorage/ as a URI to access an SD
// card over OTG cable. Other models, like Galaxy S5, use /storage/UsbDriveA
// "/mnt/usb_storage",
// "/mnt/UsbDriveA",
// "/mnt/UsbDriveB",
};
/** Find path to removable SD card. */
public static File findSdCardPath(Context context) {
String[] mountFields;
BufferedReader bufferedReader = null;
String lineRead = null;
/** Possible SD card paths */
LinkedHashSet<File> candidatePaths = new LinkedHashSet<>();
/** Build a list of candidate paths, roughly in order of preference. That way if
* we can't definitively detect removable storage, we at least can pick a more likely
* candidate. */
// Could do: use getExternalStorageState(File path), with and without an argument, when
// available. With an argument is available since API level 21.
// This may not be necessary, since we also check whether a directory exists and has contents,
// which would fail if the external storage state is neither MOUNTED nor MOUNTED_READ_ONLY.
// I moved hard-coded paths toward the end, but we need to make sure we put the ones in
// backwards order that are returned by the OS. And make sure the iterators respect
// the order!
// This is because when multiple "external" storage paths are returned, it's always (in
// experience, but not guaranteed by documentation) with internal/emulated storage
// first, removable storage second.
// Add value of environment variables as candidates, if set:
// EXTERNAL_STORAGE, SECONDARY_STORAGE, EXTERNAL_SDCARD_STORAGE
// But note they are *not* necessarily *removable* storage! Especially EXTERNAL_STORAGE.
// And they are not documented (API) features. Typically useful only for old versions of Android.
String val = System.getenv("SECONDARY_STORAGE");
if (!TextUtils.isEmpty(val)) addPath(val, null, candidatePaths);
val = System.getenv("EXTERNAL_SDCARD_STORAGE");
if (!TextUtils.isEmpty(val)) addPath(val, null, candidatePaths);
// Get listing of mounted devices with their properties.
ArrayList<File> mountedPaths = new ArrayList<>();
try {
// Note: Despite restricting some access to /proc (http://stackoverflow.com/a/38728738/423105),
// Android 7.0 does *not* block access to /proc/mounts, according to our test on George's Alcatel A30 GSM.
bufferedReader = new BufferedReader(new FileReader("/proc/mounts"));
// Iterate over each line of the mounts listing.
while ((lineRead = bufferedReader.readLine()) != null) {
Log.d(TAG, "\nMounts line: " + lineRead);
mountFields = lineRead.split(" ");
// columns: device, mountpoint, fs type, options... Example:
// /dev/block/vold/179:97 /storage/sdcard1 vfat rw,dirsync,nosuid,nodev,noexec,relatime,uid=1000,gid=1015,fmask=0002,dmask=0002,allow_utime=0020,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0
String device = mountFields[0], path = mountFields[1], fsType = mountFields[2];
// The device, path, and fs type must conform to expected patterns.
if (!(devicePattern.matcher(device).matches() &&
pathPattern.matcher(path).matches() &&
fsTypePattern.matcher(fsType).matches()) ||
// mtdblock is internal, I'm told.
device.contains("mtdblock") ||
// Check for disqualifying patterns in the path.
pathAntiPattern.matcher(path).matches()) {
// If this mounts line fails our tests, skip it.
continue;
}
// TODO maybe: check options to make sure it's mounted RW?
// The answer at http://stackoverflow.com/a/13648873/423105 does.
// But it hasn't seemed to be necessary so far in my testing.
// This line met the criteria so far, so add it to candidate list.
addPath(path, null, mountedPaths);
}
} catch (IOException ignored) {
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException ignored) {
}
}
}
// Append the paths from mount table to candidate list, in reverse order.
if (!mountedPaths.isEmpty()) {
// See https://stackoverflow.com/a/5374346/423105 on why the following is necessary.
// Basically, .toArray() needs its parameter to know what type of array to return.
File[] mountedPathsArray = mountedPaths.toArray(new File[mountedPaths.size()]);
addAncestors(candidatePaths, mountedPathsArray);
}
// Add hard-coded known common paths to candidate list:
addStrings(candidatePaths, commonPaths);
// If the above doesn't work we could try the following other options, but in my experience they
// haven't added anything helpful yet.
// getExternalFilesDir() and getExternalStorageDirectory() typically something app-specific like
// /storage/sdcard1/Android/data/com.mybackuparchives.android/files
// so we want the great-great-grandparent folder.
// This may be non-removable.
Log.d(TAG, "Environment.getExternalStorageDirectory():");
addPath(null, ancestor(Environment.getExternalStorageDirectory()), candidatePaths);
// Context.getExternalFilesDirs() is only available from API level 19. You can use
// ContextCompat.getExternalFilesDirs() on earlier APIs, but it only returns one dir anyway.
Log.d(TAG, "context.getExternalFilesDir(null):");
addPath(null, ancestor(context.getExternalFilesDir(null)), candidatePaths);
// "Returns absolute paths to application-specific directories on all external storage
// devices where the application can place persistent files it owns."
// We might be able to use these to deduce a higher-level folder that isn't app-specific.
// Also, we apparently have to call getExternalFilesDir[s](), at least in KITKAT+, in order to ensure that the
// "external files" directory exists and is available.
Log.d(TAG, "ContextCompat.getExternalFilesDirs(context, null):");
addAncestors(candidatePaths, ContextCompat.getExternalFilesDirs(context, null));
// Very similar results:
Log.d(TAG, "ContextCompat.getExternalCacheDirs(context):");
addAncestors(candidatePaths, ContextCompat.getExternalCacheDirs(context));
// TODO maybe: use getExternalStorageState(File path), with and without an argument, when
// available. With an argument is available since API level 21.
// This may not be necessary, since we also check whether a directory exists,
// which would fail if the external storage state is neither MOUNTED nor MOUNTED_READ_ONLY.
// A "public" external storage directory. But in my experience it doesn't add anything helpful.
// Note that you can't pass null, or you'll get an NPE.
final File publicDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
// Take the parent, because we tend to get a path like /pathTo/sdCard/Music.
addPath(null, publicDirectory.getParentFile(), candidatePaths);
// EXTERNAL_STORAGE: may not be removable.
val = System.getenv("EXTERNAL_STORAGE");
if (!TextUtils.isEmpty(val)) addPath(val, null, candidatePaths);
if (candidatePaths.isEmpty()) {
Log.w(TAG, "No removable microSD card found.");
return null;
} else {
Log.i(TAG, "\nFound potential removable storage locations: " + candidatePaths);
}
// Accept or eliminate candidate paths if we can determine whether they're removable storage.
// In Lollipop and later, we can check isExternalStorageRemovable() status on each candidate.
if (Build.VERSION.SDK_INT >= 21) {
Iterator<File> itf = candidatePaths.iterator();
while (itf.hasNext()) {
File dir = itf.next();
// handle illegalArgumentException if the path is not a valid storage device.
try {
if (Environment.isExternalStorageRemovable(dir)
// && containsKnownFile(dir)
) {
Log.i(TAG, dir.getPath() + " is removable external storage");
return dir;
} else if (Environment.isExternalStorageEmulated(dir)) {
Log.d(TAG, "Removing emulated external storage dir " + dir);
itf.remove();
}
} catch (IllegalArgumentException e) {
Log.d(TAG, "isRemovable(" + dir.getPath() + "): not a valid storage device.", e);
}
}
}
// Continue trying to accept or eliminate candidate paths based on whether they're removable storage.
// On pre-Lollipop, we only have singular externalStorage. Check whether it's removable.
if (Build.VERSION.SDK_INT >= 9) {
File externalStorage = Environment.getExternalStorageDirectory();
Log.d(TAG, String.format(Locale.ROOT, "findSDCardPath: getExternalStorageDirectory = %s", externalStorage.getPath()));
if (Environment.isExternalStorageRemovable()) {
// Make sure this is a candidate.
// TODO: Does this contains() work? Should we be canonicalizing paths before comparing?
if (candidatePaths.contains(externalStorage)
// && containsKnownFile(externalStorage)
) {
Log.d(TAG, "Using externalStorage dir " + externalStorage);
return externalStorage;
}
} else if (Build.VERSION.SDK_INT >= 11 && Environment.isExternalStorageEmulated()) {
Log.d(TAG, "Removing emulated external storage dir " + externalStorage);
candidatePaths.remove(externalStorage);
}
}
// If any directory contains our special test file, consider that the microSD card.
if (KNOWNFILE != null) {
for (File dir : candidatePaths) {
Log.d(TAG, String.format(Locale.ROOT, "findSdCardPath: Looking for known file in candidate path, %s", dir));
if (containsKnownFile(dir)) return dir;
}
}
// If we don't find the known file, still try taking the first candidate.
if (!candidatePaths.isEmpty()) {
Log.d(TAG, "No definitive path to SD card; taking the first realistic candidate.");
return candidatePaths.iterator().next();
}
// If no reasonable path was found, give up.
return null;
}
/** Add each path to the collection. */
private static void addStrings(LinkedHashSet<File> candidatePaths, String[] newPaths) {
for (String path : newPaths) {
addPath(path, null, candidatePaths);
}
}
/** Add ancestor of each File to the collection. */
private static void addAncestors(LinkedHashSet<File> candidatePaths, File[] files) {
for (int i = files.length - 1; i >= 0; i--) {
addPath(null, ancestor(files[i]), candidatePaths);
}
}
/**
* Add a new candidate directory path to our list, if it's not obviously wrong.
* Supply path as either String or File object.
* @param strNew path of directory to add (or null)
* @param fileNew directory to add (or null)
*/
private static void addPath(String strNew, File fileNew, Collection<File> paths) {
// If one of the arguments is null, fill it in from the other.
if (strNew == null) {
if (fileNew == null) return;
strNew = fileNew.getPath();
} else if (fileNew == null) {
fileNew = new File(strNew);
}
if (!paths.contains(fileNew) &&
// Check for paths known not to be removable SD card.
// The antipattern check can be redundant, depending on where this is called from.
!pathAntiPattern.matcher(strNew).matches()) {
// Eliminate candidate if not a directory or not fully accessible.
if (fileNew.exists() && fileNew.isDirectory() && fileNew.canExecute()) {
Log.d(TAG, " Adding candidate path " + strNew);
paths.add(fileNew);
} else {
Log.d(TAG, String.format(Locale.ROOT, " Invalid path %s: exists: %b isDir: %b canExec: %b canRead: %b",
strNew, fileNew.exists(), fileNew.isDirectory(), fileNew.canExecute(), fileNew.canRead()));
}
}
}
private static final String ANDROID_DIR = File.separator + "Android";
private static File ancestor(File dir) {
// getExternalFilesDir() and getExternalStorageDirectory() typically something app-specific like
// /storage/sdcard1/Android/data/com.mybackuparchives.android/files
// so we want the great-great-grandparent folder.
if (dir == null) {
return null;
} else {
String path = dir.getAbsolutePath();
int i = path.indexOf(ANDROID_DIR);
if (i == -1) {
return dir;
} else {
return new File(path.substring(0, i));
}
}
}
/** Returns true iff dir contains the special test file.
* Assumes that dir exists and is a directory. (Is this a necessary assumption?) */
private static boolean containsKnownFile(File dir) {
if (KNOWNFILE == null) return false;
File knownFile = new File(dir, KNOWNFILE);
return knownFile.exists();
}
private static Pattern
/** Pattern that SD card device should match */
devicePattern = Pattern.compile("/dev/(block/.*vold.*|fuse)|/mnt/.*"),
/** Pattern that SD card mount path should match */
pathPattern = Pattern.compile("/(mnt|storage|external_sd|extsd|_ExternalSD|Removable|.*MicroSD).*",
Pattern.CASE_INSENSITIVE),
/** Pattern that the mount path should not match.
* 'emulated' indicates an internal storage location, so skip it.
* 'asec' is an encrypted package file, decrypted and mounted as a directory. */
pathAntiPattern = Pattern.compile(".*(/secure|/asec|/emulated).*"),
/** These are expected fs types, including vfat. tmpfs is not OK.
* fuse can be removable SD card (as on Moto E or Asus ZenPad), or can be internal (Huawei G610). */
fsTypePattern = Pattern.compile(".*(fat|msdos|ntfs|ext[34]|fuse|sdcard|esdfs).*");
}
P.S.
Don't forget <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> in the manifest. And at API level 23 and higher, make sure to use checkSelfPermission / requestPermissions. Set KNOWNFILE="myappfile" if there's a file or folder you expect to find on the SD card. That makes detection more accurate. Obviously you'll want to cache the value of findSdCardPath(), rather than recomputing it every time you need it. There's a bunch of logging (Log.d()) in the above code. It helps diagnose any cases where the right path isn't found. Comment it out if you don't want logging.