ANDROID: Added more logic for scraping the storage paths
This commit is contained in:
parent
3c92722db6
commit
27fbde1443
1 changed files with 327 additions and 4 deletions
|
@ -6,6 +6,19 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
import android.util.Log;
|
||||
import android.os.Build;
|
||||
|
||||
|
||||
/**
|
||||
* Contains helper methods to get list of available media
|
||||
*/
|
||||
|
@ -14,6 +27,308 @@ public class ExternalStorage {
|
|||
public static final String EXTERNAL_SD_CARD = "externalSdCard";
|
||||
public static final String DATA_DIRECTORY = "ScummVM data directory";
|
||||
|
||||
|
||||
// Find candidate removable sd card paths
|
||||
// Code reference: https://stackoverflow.com/a/54411385
|
||||
private static final String ANDROID_DIR = File.separator + "Android";
|
||||
|
||||
private static String ancestor(File dir) {
|
||||
// getExternalFilesDir() and getExternalStorageDirectory()
|
||||
// may return 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 path;
|
||||
} else {
|
||||
return path.substring(0, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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).*");
|
||||
|
||||
/** 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 LinkedHashSet<File> findSdCardPath() {
|
||||
String[] mountFields;
|
||||
BufferedReader bufferedReader = null;
|
||||
String lineRead = null;
|
||||
|
||||
/** Possible SD card paths */
|
||||
LinkedHashSet<File> candidatePaths = new LinkedHashSet<File>();
|
||||
|
||||
/** 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, candidatePaths);
|
||||
}
|
||||
|
||||
val = System.getenv("EXTERNAL_SDCARD_STORAGE");
|
||||
if (!TextUtils.isEmpty(val)) {
|
||||
addPath(val, candidatePaths);
|
||||
}
|
||||
|
||||
// Get listing of mounted devices with their properties.
|
||||
ArrayList<File> mountedPaths = new ArrayList<File>();
|
||||
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(ScummVM.LOG_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.
|
||||
// mtdblock is internal, I'm told.
|
||||
// Check for disqualifying patterns in the path.
|
||||
// If this mounts line fails our tests, skip it.
|
||||
if (!(devicePattern.matcher(device).matches()
|
||||
&& pathPattern.matcher(path).matches()
|
||||
&& fsTypePattern.matcher(fsType).matches())
|
||||
|| device.contains("mtdblock")
|
||||
|| pathAntiPattern.matcher(path).matches()
|
||||
) {
|
||||
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, 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(mountedPathsArray, candidatePaths);
|
||||
}
|
||||
|
||||
// Add hard-coded known common paths to candidate list:
|
||||
addStrings(commonPaths, candidatePaths);
|
||||
|
||||
// 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(ScummVM.LOG_TAG, "Environment.getExternalStorageDirectory():");
|
||||
addPath(ancestor(Environment.getExternalStorageDirectory()), candidatePaths);
|
||||
|
||||
// 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(publicDirectory.getParentFile().getAbsolutePath(), candidatePaths);
|
||||
// EXTERNAL_STORAGE: may not be removable.
|
||||
val = System.getenv("EXTERNAL_STORAGE");
|
||||
if (!TextUtils.isEmpty(val)) {
|
||||
addPath(val, candidatePaths);
|
||||
}
|
||||
|
||||
if (candidatePaths.isEmpty()) {
|
||||
Log.w(ScummVM.LOG_TAG, "No removable microSD card found.");
|
||||
return candidatePaths;
|
||||
} else {
|
||||
Log.i(ScummVM.LOG_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)) {
|
||||
Log.i(ScummVM.LOG_TAG, dir.getPath() + " is removable external storage");
|
||||
addPath(dir.getAbsolutePath(), candidatePaths);
|
||||
} else if (Environment.isExternalStorageEmulated(dir)) {
|
||||
Log.d(ScummVM.LOG_TAG, "Removing emulated external storage dir " + dir);
|
||||
itf.remove();
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.d(ScummVM.LOG_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(ScummVM.LOG_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)) {
|
||||
Log.d(ScummVM.LOG_TAG, "Using externalStorage dir " + externalStorage);
|
||||
// return externalStorage;
|
||||
addPath(externalStorage.getAbsolutePath(), candidatePaths);
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= 11 && Environment.isExternalStorageEmulated()) {
|
||||
Log.d(ScummVM.LOG_TAG, "Removing emulated external storage dir " + externalStorage);
|
||||
candidatePaths.remove(externalStorage);
|
||||
}
|
||||
}
|
||||
|
||||
return candidatePaths;
|
||||
}
|
||||
|
||||
|
||||
/** Add each path to the collection. */
|
||||
private static void addStrings(String[] newPaths, LinkedHashSet<File> candidatePaths) {
|
||||
for (String path : newPaths) {
|
||||
addPath(path, candidatePaths);
|
||||
}
|
||||
}
|
||||
|
||||
/** Add ancestor of each File to the collection. */
|
||||
private static void addAncestors(File[] files, LinkedHashSet<File> candidatePaths) {
|
||||
for (int i = files.length - 1; i >= 0; i--) {
|
||||
addPath(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
|
||||
*/
|
||||
private static void addPath(String strNew, Collection<File> paths) {
|
||||
// If one of the arguments is null, fill it in from the other.
|
||||
if (strNew != null && !strNew.isEmpty()) {
|
||||
File 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(ScummVM.LOG_TAG, " Adding candidate path " + strNew);
|
||||
paths.add(fileNew);
|
||||
} else {
|
||||
Log.d(ScummVM.LOG_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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @return True if the external storage is available. False otherwise.
|
||||
*/
|
||||
|
@ -135,9 +450,7 @@ public class ExternalStorage {
|
|||
map.add(Environment.getDataDirectory().getAbsolutePath());
|
||||
|
||||
// Now go through the external storage
|
||||
String state = Environment.getExternalStorageState();
|
||||
|
||||
if (Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state) ) { // we can read the External Storage...
|
||||
if (isAvailable()) { // we can read the External Storage...
|
||||
// Retrieve the primary External Storage:
|
||||
File primaryExternalStorage = Environment.getExternalStorageDirectory();
|
||||
|
||||
|
@ -154,7 +467,7 @@ public class ExternalStorage {
|
|||
File externalStorageRoot = new File(externalStorageRootDir);
|
||||
File[] files = externalStorageRoot.listFiles();
|
||||
|
||||
if (files.length > 0) {
|
||||
if (files != null) {
|
||||
for (final File file : files) {
|
||||
if (file.isDirectory() && file.canRead() && (file.listFiles().length > 0)) { // it is a real directory (not a USB drive)...
|
||||
String key = file.getAbsolutePath();
|
||||
|
@ -168,6 +481,16 @@ public class ExternalStorage {
|
|||
}
|
||||
}
|
||||
|
||||
// Get candidates for removable external storage
|
||||
LinkedHashSet<File> candidateRemovableSdCardPaths = findSdCardPath();
|
||||
for (final File file : candidateRemovableSdCardPaths) {
|
||||
String key = file.getAbsolutePath();
|
||||
if (!map.contains(key)) {
|
||||
map.add(key); // Make name as directory
|
||||
map.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue