Разрабатывая приложение для проведения соревнований, я столкнулся с проблемой хранения базы данных. Проблема состояла в том, как мне определить внешнюю карту памяти. В целом поиск в сети точного ответа не дал. Поэтому, объединив все найденные результаты, я собрал свой класс. Если кому интересно, смотрим под катом.
Итак, начнем с теории.
Терминология
Гугл нам говорит, что есть следующие понятия:
- Внутренняя (internal) память — это часть встроенной в телефон карты памяти. При ее использовании по умолчанию папка приложения защищена от доступа других приложений (Using the Internal Storage).
- Внешняя (external) память — это общее «внешнее хранилище», т.е. это может быть как часть встроенной памяти, так и удаляемое устройство. Обычно это часть встроенной памяти, как удаляемое устройство я видел в последний раз на андройде 2.2, где встроенная память была около 2Гб, и подключаемая память становилась внешней (Using the External Storage).
- Удаляемая (removable) память — все хранилища, которые могут быть удалены из устройства без «хирургических» вмешательств.
До версии KitKat 4.4 API не предоставляло функционала для получения путей к внешней памяти. Начиная с этой версии (API 19) появилась функция public abstract File[] getExternalFilesDirs (String type), которая возвращает массив строк с путями к внутренней и внешней памяти. Но как же быть с нашей SD Card, которая вставлена в слот? Путь к ней мы опять не можем получить.
Результаты поиска
Чтобы ответить на поставленный вопрос я обратился к всезнающему гуглу. Но и он мне не дал четкого ответа. Было рассмотрено множество вариантов определения от использования стандартных функций, которые ведут к внешней памяти, но ничего общего с удаляемыми устройствами хранения данных они не имеют, до обработки правил монтирования устройств (Android же на ядре Linux работает). В последних случаях были использованы «зашитые» пути к папке с примонтироваными устройствами (в различных версиях эта директория разная). Не стоит забывать, что от версии к версии правила монтирования меняются.
В конечном итоге я решил объединить все полученные знания и написал свой класс, который может нам вернуть пути к внешним и удаляемым устройствам.
Описание кода
Был создан класс MountDevice, который содержит в себе путь к устройству, тип устройства и некий хэш.
Типов устройств выделено два (внутреннюю память я не стал трогать, так как к ней доступ можно получить через API системы).
public enum MountDeviceType {
EXTERNAL_SD_CARD, REMOVABLE_SD_CARD
}
И был создан класс StorageHelper, который и осуществляет поиск доступных карт памяти.
В классе StorageHelper реализовано два способа поиска — через системное окружение (Environment) и с использованием утилиты Linux mount, а точнее результата ее выполнения.
Способ первый — Environment
При работе с окружением я использую стандартную функцию getExternalStorageDirectory() для получения информации о внешней памяти. Чтобы получить информацию о удаляемой памяти, я использую переменную окружения "SECONDARY_STORAGE".
Внешняя память всегда одна и обычно всегда есть, поэтому проверяем ее на читаемость, вычисляем хэш и запоминаем. Удаляемой памяти может быть много, поэтому необходимо полученную строку разбить по разделителю и проверять каждое значение.
String path = android.os.Environment.getExternalStorageDirectory()
.getAbsolutePath();
if (!path.trim().isEmpty()
&& android.os.Environment.getExternalStorageState().equals(
android.os.Environment.MEDIA_MOUNTED)) {
testAndAdd(path, MountDeviceType.EXTERNAL_SD_CARD);
}
// Получаем ремувабл
String rawSecondaryStoragesStr = System.getenv("SECONDARY_STORAGE");
if (rawSecondaryStoragesStr != null
&& !rawSecondaryStoragesStr.isEmpty()) {
// All Secondary SD-CARDs splited into array
final String[] rawSecondaryStorages = rawSecondaryStoragesStr
.split(File.pathSeparator);
for (String rawSecondaryStorage : rawSecondaryStorages) {
testAndAdd(rawSecondaryStorage,
MountDeviceType.REMOVABLE_SD_CARD);
}
}
Вариант решения взят со stackoverflow. Ответ где-то там внизу.
Способ второй — mount
Так как у меня долго не получалось заставить систему мне сказать путь к удаляемой памяти, я решил искать в сторону примонтированных устройств. В системе есть файлы конфигурации, в которых описаны правила монтирования внешних устройств. Все бы хорошо, но на Android версии 4.* к этому файлу простым смертным доступа нет, поэтому рассматривать этот способ не буду.
Вернемся к утилите mount. При запуске без параметров команда возвращает список смонтированных файловых систем. Удаляемые устройства имеют обычно формат файловой системы FAT, то будем выделять строки, в которых есть характеристика "fat". Внешняя память будет характеризоваться параметром "fuse".
Примечание: при использовании такого способа не всегда корректно (скорее всего я что-то не учел) определяются типы смотнтированных устройств. Разницу замечал на разных версиях Android. Поэтому этот способ можно использовать как дополнительный.
try {
Runtime runtime = Runtime.getRuntime();
proc = runtime.exec("mount");
try {
is = proc.getInputStream();
isr = new InputStreamReader(is);
br = new BufferedReader(isr);
while ((line = br.readLine()) != null) {
if (line.contains("secure"))
continue;
if (line.contains("asec"))
continue;
if (line.contains("fat")) {// TF card
String columns[] = line.split(" ");
if (columns != null && columns.length > 1) {
testAndAdd(columns[1],
MountDeviceType.REMOVABLE_SD_CARD);
}
} else if (line.contains("fuse")) {// internal(External)
// storage
String columns[] = line.split(" ");
if (columns != null && columns.length > 1) {
// mount = mount.concat(columns[1] + "n");
testAndAdd(columns[1],
MountDeviceType.EXTERNAL_SD_CARD);
}
}
}
} finally {
...
}
} catch (Exception e) {
...
}
Вариант решения взят со stackoverflow. Ответов там несколько примерно одинаковых.
Про дублирование
Многие замечали в директории монтирования устройств такую картину:
/storage/sdcard0/
/storage/emulated/0/
/storage/emulated/legacy/
И что самое интересно, все это одна и та же внешняя карта памяти. Такое дробление начинается с версии Jelly Bean и сделано это для поддержки многопользовательского режима работы системы. Более подробно тут. И вот, чтобы не получать одну и туже карту памяти как различные устройства, необходим способ определения идентичности. Если бы был доступ к конфигурации монтирования, то и вопросов не было. Но доступа нет. Поэтому я тут подсмотрел решение с расчетом хэша для каждого устройства:
- создаем StringBuilder
- записываем в него общий размер устройства и размер используемого пространства устройства
- обходим содержимое корня устройства
- записываем имя каталога
- записываем имя файла и размер
- вычисляем hash
private int calcHash(File dir) {
StringBuilder tmpHash = new StringBuilder();
tmpHash.append(dir.getTotalSpace());
tmpHash.append(dir.getUsableSpace());
File[] list = dir.listFiles();
for (File file : list) {
tmpHash.append(file.getName());
if (file.isFile()) {
tmpHash.append(file.length());
}
}
return tmpHash.toString().hashCode();
}
Пример использования
/* Получаем базовый путь */
if (!mPreferences.contains(PREFS_BASEBATH)) {
// Если еще не сохранялся в настройках, то пытаемся найти карты
// памяти
ArrayList<MountDevice> storages = StorageHelper.getInstance()
.getRemovableMountedDevices();
// проверяем съемные карты памяти
if (storages.size() != 0) {
setBasePath(storages.get(0).getPath() + mAppPath);
} else if ((storages = StorageHelper.getInstance() // Проверяем
// внутреннюю
// память
.getExternalMountedDevices()).size() != 0) {
setBasePath(storages.get(0).getPath() + mAppPath);
}
} else {
// Вытаскиваем из сохранненых настроек
mBasePath = mPreferences.getString(PREFS_BASEBATH, context
.getFilesDir().getParent());
}
Заключение
Подробные рассуждения по этому вопросу понимания памяти в Android, некоторые советы можно прочитать тут.
Исходный код всего класса расположен еще нигде не расположен. На днях постараюсь разместить на gitHub.
Кто еще какими способами пользуется?
Автор: vait