Итак, начнем с теории.
Терминология
Гугл нам говорит, что есть следующие понятия:
- Внутренняя (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, некоторые советы можно прочитать тут.
Исходный код всего класса
Кто еще какими способами пользуется?
UPD1: Исходный код класса на bitbucket
Комментарии (16)
Buggins
03.04.2015 16:44А на свежих андроидах обычное (не предустановленное) приложение может писать на SD карту?
vait Автор
04.04.2015 21:30Если без доп. прав, то приложение может создавать каталоги, файлы, но вот удалять оно не может.
Проверял на самсунге с Андройдом 4.4.4. Рута нет, конфиги не правились.
recompileme
06.04.2015 10:56Перепробовал много вариантов, остановился на тупом переборе: getExternalSdCardPath
vait Автор
07.04.2015 10:06+1Например на моем аппарате в папке /mnt/ есть папка «sdcard» — она ссылается на внешнюю память. Карты памяти в /mnt/ не примонтировано. Мне кажется, это не самый лучший способ.
Замечательный и самый лучший, на мой взгляд, вариант был бы через карту «vold.fstab». Но увы, на последних версиях андройда (с 4.3) доступ к этому файлу без рута не получить. Да и называется он по-другому, и расположен в другом месте:
For Android 4.2.2 and earlier, the device-specific vold.fstab configuration file defines mappings from sysfs devices to filesystem mount points
For Android releases 4.3 and later, the various fstab files used by init, vold and recovery were unified in the /fstab.<device> file.
Источник: source.android.com
CAJAX
А зачем может понадобиться путь к корню флешке вместо Environment.getExternalStorageDirectory()?
vait Автор
Сейчас это не очень актуально, но в теории, карты памяти больше, чем встроенная «внешняя» память. Поэтому потенциально большую базу лучше хранить на карте памяти. Я из таких соображений стал искать альтернативу.
В настоящее время у меня LG L65. У него заявлено 4 ГБ памяти, из них пользователю доступно всего 1,5. Ставим приложения, мелодии для звонка и т.п. и остается всего 200 Мб.
UPD: Viber, к примеру, все хранит во внешней памяти. Соответственно место постоянно уменьшается.
jimpanzer
Я думаю вайбер хранит там только объемные медиа-файлы, а не всю переписку. Может быть именно для большого количества относительно больших файлов это и актуально, но точно не для «потенциально большой базы».
1) Насколько должна быть большая база чтобы getExternalStorageDirectory() не удовлетворяло полностью? (я надеюсь вы медиа-файлы в blob-ах не храните)
2) Если база действительно большая, то она хранит огромное количество записей. Я не думаю, что стоит светить эти записи всем пользователям (даже без рута).
vait Автор
Согласен с Вами, моя формулировка «потенциально большой базы» не совсем корректна. В общем мы друг друга поняли.
CAJAX
Ух, не знал о таком. Буду иметь в виду.
YoungSkipper
Я вот тоже не понимаю, но сколько было возмущенных пользователей которые кричали чтобы игра скачивала ресурсы именно на флешку, а не в Environment.getExternalStorageDirectory() — мы пока не сделали имели тысячи единиц в отзывах из-за этого. И кстати, все вышеописанные шаманства все равно не спасают и будут экзотические девайсы в которых определить верно не получится — поэтому еще и возможность руками ввести путь добавили.
jimpanzer
Эм… Мне кажется или специально для этого есть Expansion File?
Не могу понять недовольство ваших пользователей. Вполне нормальная практика.
YoungSkipper
Недовольство очень простое — внутренней памяти на дешевом телефоне допустим 4 гигабайта — и ее постоянно не хватает, а карточку можно поставить на 32 гигабайта.
obb не помогут — на тех девайсах на которых getExternalStorageDirectory указывает на внутреннюю память при наличии SD карточки, obb тоже будут качаться на внутреннюю память