package com.docd.purefm.utils;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.StringTokenizer;
import android.os.Environment;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import android.support.annotation.NonNull;
/**
* Provides methods for working with storages
*/
public final class StorageHelper {
//private static final String TAG = "StorageHelper";
private StorageHelper() {
}
private static final String[] AVOIDED_DEVICES = new String[] {
"rootfs", "tmpfs", "dvpts", "proc", "sysfs", "none"
};
private static final String[] AVOIDED_DIRECTORIES = new String[] {
"obb", "asec"
};
private static final String[] DISALLOWED_FILESYSTEMS = new String[] {
"tmpfs", "rootfs", "romfs", "devpts", "sysfs", "proc", "cgroup", "debugfs"
};
private static final String STORAGES_ROOT;
static {
final String primaryStoragePath = Environment.getExternalStorageDirectory()
.getAbsolutePath();
final int index = primaryStoragePath.indexOf(File.separatorChar, 1);
if (index != -1) {
STORAGES_ROOT = primaryStoragePath.substring(0, index + 1);
} else {
STORAGES_ROOT = File.separator;
}
}
/**
* Returns a list of all mounted {@link StorageVolume}s
*
* @return list of mounted {@link StorageVolume}s
*/
@NonNull
public static List<Volume> getAllDevices() {
final List<Volume> volumeList = new ArrayList<>(20);
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("/proc/mounts"));
String line;
while ((line = reader.readLine()) != null) {
final StringTokenizer tokens = new StringTokenizer(line, " ");
final String device = tokens.nextToken();
final String path = tokens.nextToken();
final String fileSystem = tokens.nextToken();
final File file = new File(path);
final Volume volume = createVolume(device, file, fileSystem);
final StringTokenizer flags = new StringTokenizer(tokens.nextToken(), ",");
while (flags.hasMoreTokens()) {
final String token = flags.nextToken();
if (token.equals("rw")) {
volume.mReadOnly = false;
break;
} else if (token.equals("ro")) {
volume.mReadOnly = true;
break;
}
}
volumeList.add(volume);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(reader);
}
return volumeList;
}
/**
* Sets {@link StorageVolume.Type}, removable and emulated flags and adds to
* volumeList
*
* @param volumeList
* List to add volume to
* @param v
* volume to add to list
* @param includeUsb
* if false, volume with type {@link StorageVolume.Type#USB} will
* not be added
* @param asFirstItem
* if true, adds the volume at the beginning of the volumeList
*/
private static void setTypeAndAdd(final List<StorageVolume> volumeList,
final StorageVolume v,
final boolean includeUsb,
final boolean asFirstItem) {
final StorageVolume.Type type = resolveType(v);
if (includeUsb || type != StorageVolume.Type.USB) {
v.mType = type;
if (v.file.equals(Environment.getExternalStorageDirectory())) {
v.mRemovable = Environment.isExternalStorageRemovable();
} else {
v.mRemovable = type != StorageVolume.Type.INTERNAL;
}
v.mEmulated = type == StorageVolume.Type.INTERNAL;
if (asFirstItem) {
volumeList.add(0, v);
} else {
volumeList.add(v);
}
}
}
/**
* Resolved {@link StorageVolume} type
*
* @param v
* {@link StorageVolume} to resolve type for
* @return {@link StorageVolume} type
*/
private static StorageVolume.Type resolveType(final StorageVolume v) {
if (v.file.equals(Environment.getExternalStorageDirectory())
&& Environment.isExternalStorageEmulated()) {
return StorageVolume.Type.INTERNAL;
} else if (StringUtils.containsIgnoreCase(v.file.getAbsolutePath(), "usb")) {
return StorageVolume.Type.USB;
} else {
return StorageVolume.Type.EXTERNAL;
}
}
/**
* Checks whether the array contains object
*
* @param array
* Array to check
* @param object
* Object to find
* @return true, if the given array contains the object
*/
private static <T> boolean arrayContains(T[] array, T object) {
for (final T item : array) {
if (item.equals(object)) {
return true;
}
}
return false;
}
/**
* Checks whether the path contains one of the directories
*
* For example, if path is /one/two, it returns true input is "one" or
* "two". Will return false if the input is one of "one/two", "/one" or
* "/two"
*
* @param path
* path to check for a directory
* @param dirs
* directories to find
* @return true, if the path contains one of the directories
*/
private static boolean pathContainsDir(final String path, final String[] dirs) {
final StringTokenizer tokens = new StringTokenizer(path, File.separator);
while (tokens.hasMoreElements()) {
final String next = tokens.nextToken();
for (final String dir : dirs) {
if (next.equals(dir)) {
return true;
}
}
}
return false;
}
@NonNull
private static StorageHelper.Volume createVolume(
@NonNull final String device,
@NonNull final File file,
@NonNull final String fileSystem) {
// this approach considers that all storages are mounted in the same non-root directory
boolean isStorageVolume = !STORAGES_ROOT.equals(File.separator);
final String path = file.getAbsolutePath();
if (isStorageVolume && !path.startsWith(STORAGES_ROOT)) {
isStorageVolume = false;
}
if (isStorageVolume && arrayContains(AVOIDED_DEVICES, device)) {
isStorageVolume = false;
}
if (isStorageVolume && pathContainsDir(path, AVOIDED_DIRECTORIES)) {
isStorageVolume = false;
}
// ones with non-storage filesystems
if (isStorageVolume && arrayContains(DISALLOWED_FILESYSTEMS, fileSystem)) {
isStorageVolume = false;
}
// volumes that are not accessible are not storage volumes
if (isStorageVolume && !(file.canRead() && file.canExecute())) {
isStorageVolume = false;
}
return isStorageVolume ? new StorageVolume(device, file, fileSystem) :
new Volume(device, file, fileSystem);
}
/**
* Retrieves {@link StorageVolume} items from volume list
*
* @param allVolumes List to get {@link StorageVolume} from
* @return {@link StorageVolume} list from items of volume list
*/
@NonNull
public static List<StorageVolume> getStorageVolumes(@NonNull final List<Volume> allVolumes) {
final Map<String, List<StorageVolume>> deviceVolumeMap = new HashMap<>();
for (final Volume v : allVolumes) {
if (v instanceof StorageVolume) {
List<StorageVolume> volumes = deviceVolumeMap.get(v.device);
if (volumes == null) {
volumes = new ArrayList<>(3);
deviceVolumeMap.put(v.device, volumes);
}
volumes.add((StorageVolume) v);
}
}
// remove external storage volumes that are the same devices
boolean primaryStorageIncluded = false;
final File externalStorage = Environment.getExternalStorageDirectory();
final List<StorageVolume> storageVolumeList = new ArrayList<>();
for (final Entry<String, List<StorageVolume>> entry : deviceVolumeMap.entrySet()) {
final List<StorageVolume> volumes = entry.getValue();
if (volumes.size() == 1) {
// go ahead and add
final StorageVolume v = volumes.get(0);
final boolean isPrimaryStorage = v.file.equals(externalStorage);
primaryStorageIncluded |= isPrimaryStorage;
setTypeAndAdd(storageVolumeList, v, true, isPrimaryStorage);
continue;
}
final int volumesLength = volumes.size();
for (int i = 0; i < volumesLength; i++) {
final StorageVolume v = volumes.get(i);
if (v.file.equals(externalStorage)) {
primaryStorageIncluded = true;
// add as external storage and continue
setTypeAndAdd(storageVolumeList, v, true, true);
break;
}
// if that was the last one and it's not the default external
// storage then add it as is
if (i == volumesLength - 1) {
setTypeAndAdd(storageVolumeList, v, true, false);
}
}
}
// add primary storage if it was not found
if (!primaryStorageIncluded) {
final StorageVolume defaultExternalStorage = new StorageVolume("", externalStorage, "UNKNOWN");
defaultExternalStorage.mEmulated = Environment.isExternalStorageEmulated();
defaultExternalStorage.mType =
defaultExternalStorage.mEmulated ? StorageVolume.Type.INTERNAL
: StorageVolume.Type.EXTERNAL;
defaultExternalStorage.mRemovable = Environment.isExternalStorageRemovable();
defaultExternalStorage.mReadOnly =
Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED_READ_ONLY);
storageVolumeList.add(0, defaultExternalStorage);
}
return storageVolumeList;
}
/**
* Longest names first
*/
public static final Comparator<Volume> VOLUME_PATH_LENGTH_COMPARATOR = new Comparator<Volume>() {
@Override
public int compare(Volume lhs, Volume rhs) {
return rhs.file.getAbsolutePath().length() - lhs.file.getAbsolutePath().length();
}
};
/**
* Represents Volume from /proc/mounts
*/
public static class Volume {
/**
* Device name
*/
public final String device;
/**
* Points to mount point of this device
*/
public final File file;
/**
* File system of this device
*/
public final String fileSystem;
/**
* if true, the storage is mounted as read-only
*/
boolean mReadOnly;
Volume(@NonNull final String device,
@NonNull final File file,
@NonNull final String fileSystem) {
this.device = device;
this.file = file;
this.fileSystem = fileSystem;
}
/**
* Returns true if this storage is mounted as read-only
*
* @return true if this storage is mounted as read-only
*/
public boolean isReadOnly() {
return mReadOnly;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + file.hashCode();
return result;
}
/**
* Returns true if the other object is StorageHelper and it's
* {@link #file} matches this one's
*
* @see Object#equals(Object)
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final StorageVolume other = (StorageVolume) obj;
return file.equals(other.file);
}
@Override
public String toString() {
return file.getAbsolutePath() + (mReadOnly ? " ro " : " rw ") + fileSystem;
}
}
/**
* Represents storage volume information
*/
public static final class StorageVolume extends Volume {
/**
* Represents {@link StorageVolume} type
*/
public enum Type {
/**
* Device built-in internal storage. Probably points to
* {@link Environment#getExternalStorageDirectory()}
*/
INTERNAL,
/**
* External storage. Probably removable, if no other
* {@link StorageVolume} of type {@link #INTERNAL} is returned by
* {@link StorageHelper#getStorageVolumes(List)}, this might be
* pointing to {@link Environment#getExternalStorageDirectory()}
*/
EXTERNAL,
/**
* Removable usb storage
*/
USB
}
/**
* If true, the storage is removable
* Defaults to true since there is no way to determine whether the volume is removable
* except for {@link android.os.Environment#getExternalStorageDirectory()}
*/
private boolean mRemovable = true;
/**
* If true, the storage is emulated
*/
private boolean mEmulated;
/**
* Type of this storage
*/
private Type mType;
StorageVolume(@NonNull final String device,
@NonNull final File file,
@NonNull final String fileSystem) {
super(device, file, fileSystem);
}
/**
* Returns type of this storage
*
* @return Type of this storage
*/
public Type getType() {
return mType;
}
/**
* Returns true if this storage is removable
*
* @return true if this storage is removable
*/
public boolean isRemovable() {
return mRemovable;
}
/**
* Returns true if this storage is emulated
*
* @return true if this storage is emulated
*/
public boolean isEmulated() {
return mEmulated;
}
@Override
public String toString() {
return super.toString() + " " + mType + (mRemovable ? " R " : "")
+ (mEmulated ? " E " : "");
}
}
}