diff options
-rw-r--r-- | backends/platform/android/org/scummvm/scummvm/ExternalStorage.java | 331 |
1 files changed, 327 insertions, 4 deletions
diff --git a/backends/platform/android/org/scummvm/scummvm/ExternalStorage.java b/backends/platform/android/org/scummvm/scummvm/ExternalStorage.java index b9e7bf0cd1..01482a3d5a 100644 --- a/backends/platform/android/org/scummvm/scummvm/ExternalStorage.java +++ b/backends/platform/android/org/scummvm/scummvm/ExternalStorage.java @@ -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; } } |