Разрабатывая приложение для проведения соревнований, я столкнулся с проблемой хранения базы данных. Проблема состояла в том, как мне определить внешнюю карту памяти. В целом поиск в сети точного ответа не дал. Поэтому, объединив все найденные результаты, я собрал свой класс. Если кому интересно, смотрим под катом.

Итак, начнем с теории.

Терминология


Гугл нам говорит, что есть следующие понятия:
  1. Внутренняя (internal) память — это часть встроенной в телефон карты памяти. При ее использовании по умолчанию папка приложения защищена от доступа других приложений (Using the Internal Storage).
  2. Внешняя (external) память — это общее «внешнее хранилище», т.е. это может быть как часть встроенной памяти, так и удаляемое устройство. Обычно это часть встроенной памяти, как удаляемое устройство я видел в последний раз на андройде 2.2, где встроенная память была около 2Гб, и подключаемая память становилась внешней (Using the External Storage).
  3. Удаляемая (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".

Внешняя память всегда одна и обычно всегда есть, поэтому проверяем ее на читаемость, вычисляем хэш и запоминаем. Удаляемой памяти может быть много, поэтому необходимо полученную строку разбить по разделителю и проверять каждое значение.

Функция fillDevicesEnvirement
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. Поэтому этот способ можно использовать как дополнительный.

Функция fillDevicesProcess
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 и сделано это для поддержки многопользовательского режима работы системы. Более подробно тут. И вот, чтобы не получать одну и туже карту памяти как различные устройства, необходим способ определения идентичности. Если бы был доступ к конфигурации монтирования, то и вопросов не было. Но доступа нет. Поэтому я тут подсмотрел решение с расчетом хэша для каждого устройства:
  1. создаем StringBuilder
  2. записываем в него общий размер устройства и размер используемого пространства устройства
  3. обходим содержимое корня устройства
  4. записываем имя каталога
  5. записываем имя файла и размер
  6. вычисляем hash


Своя функция расчета хэша calcHash
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.

Кто еще какими способами пользуется?

UPD1: Исходный код класса на bitbucket

Комментарии (16)


  1. CAJAX
    03.04.2015 10:34

    А зачем может понадобиться путь к корню флешке вместо Environment.getExternalStorageDirectory()?


    1. vait Автор
      03.04.2015 10:39
      +2

      Сейчас это не очень актуально, но в теории, карты памяти больше, чем встроенная «внешняя» память. Поэтому потенциально большую базу лучше хранить на карте памяти. Я из таких соображений стал искать альтернативу.
      В настоящее время у меня LG L65. У него заявлено 4 ГБ памяти, из них пользователю доступно всего 1,5. Ставим приложения, мелодии для звонка и т.п. и остается всего 200 Мб.
      UPD: Viber, к примеру, все хранит во внешней памяти. Соответственно место постоянно уменьшается.


      1. jimpanzer
        03.04.2015 11:18

        Я думаю вайбер хранит там только объемные медиа-файлы, а не всю переписку. Может быть именно для большого количества относительно больших файлов это и актуально, но точно не для «потенциально большой базы».
        1) Насколько должна быть большая база чтобы getExternalStorageDirectory() не удовлетворяло полностью? (я надеюсь вы медиа-файлы в blob-ах не храните)
        2) Если база действительно большая, то она хранит огромное количество записей. Я не думаю, что стоит светить эти записи всем пользователям (даже без рута).


        1. vait Автор
          03.04.2015 13:04

          Согласен с Вами, моя формулировка «потенциально большой базы» не совсем корректна. В общем мы друг друга поняли.


      1. CAJAX
        03.04.2015 14:44

        Ух, не знал о таком. Буду иметь в виду.


    1. YoungSkipper
      03.04.2015 11:33

      Я вот тоже не понимаю, но сколько было возмущенных пользователей которые кричали чтобы игра скачивала ресурсы именно на флешку, а не в Environment.getExternalStorageDirectory() — мы пока не сделали имели тысячи единиц в отзывах из-за этого. И кстати, все вышеописанные шаманства все равно не спасают и будут экзотические девайсы в которых определить верно не получится — поэтому еще и возможность руками ввести путь добавили.


      1. jimpanzer
        03.04.2015 11:52

        Эм… Мне кажется или специально для этого есть Expansion File?

        The specific location for your expansion files is:
        <shared-storage>/Android/obb/<package-name>
        <shared-storage> is the path to the shared storage space, available from getExternalStorageDirectory().
        <package-name> is your application's Java-style package name, available from getPackageName().

        Не могу понять недовольство ваших пользователей. Вполне нормальная практика.


        1. YoungSkipper
          03.04.2015 11:57
          +2

          Недовольство очень простое — внутренней памяти на дешевом телефоне допустим 4 гигабайта — и ее постоянно не хватает, а карточку можно поставить на 32 гигабайта.

          obb не помогут — на тех девайсах на которых getExternalStorageDirectory указывает на внутреннюю память при наличии SD карточки, obb тоже будут качаться на внутреннюю память


  1. Buggins
    03.04.2015 16:44

    А на свежих андроидах обычное (не предустановленное) приложение может писать на SD карту?


    1. JaLoveAst1k
      03.04.2015 23:25

      Да, если выдать ему пермишн.


    1. vait Автор
      04.04.2015 21:30

      Если без доп. прав, то приложение может создавать каталоги, файлы, но вот удалять оно не может.
      Проверял на самсунге с Андройдом 4.4.4. Рута нет, конфиги не правились.


      1. ivanra
        05.04.2015 10:34

        должны быть все права в «своем» каталоге: SD_CARD/Android/data/my_package


        1. vait Автор
          05.04.2015 16:41

          Я проверял через файловый менеджер в корне каталога. Мои телефоны все рутированные и с правленным конфигами.


  1. Valeftin
    03.04.2015 20:39

    /storage/sdcard0/
    /storage/emulated/0/

    У меня на Sony — внутренняя


  1. recompileme
    06.04.2015 10:56

    Перепробовал много вариантов, остановился на тупом переборе: getExternalSdCardPath


    1. 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