Подтвердить что ты не робот

Как получить доступ ко всем SD-картам, используя новый Lollipop API?

фон

Начиная с Lollipop, приложения могут получить доступ к реальным SD-картам (после того, как они были недоступны в Kitkat и официально не поддерживались в предыдущих версиях), как я спросил о .

Проблема

Поскольку стало довольно редко видеть устройство Lollipop, поддерживающее SD-карту, и потому что эмулятор действительно не обладает способностью (или не так ли?), чтобы эмулировать поддержку SD-карты, мне потребовалось довольно много времени чтобы проверить его.

В любом случае кажется, что вместо обычного класса File для доступа к SD-карте (после получения разрешения для него) вам нужно использовать Uris для него, используя DocumentFile.

Это ограничивает доступ к обычным путям, поскольку я не могу найти способ конвертировать Uris в пути и наоборот (плюс это довольно раздражает). Это также означает, что я не знаю, как проверить, доступны ли текущие SD-карты, поэтому я не знаю, когда спросить у пользователя разрешение на чтение/запись на него (или на них).

Что я пробовал

В настоящее время я получаю пути ко всем SD-картам:

  /**
   * returns a list of all available sd cards paths, or null if not found.
   *
   * @param includePrimaryExternalStorage set to true if you wish to also include the path of the primary external storage
   */
  @TargetApi(Build.VERSION_CODES.HONEYCOMB)
  public static List<String> getExternalStoragePaths(final Context context,final boolean includePrimaryExternalStorage)
    {
    final File primaryExternalStorageDirectory=Environment.getExternalStorageDirectory();
    final List<String> result=new ArrayList<>();
    final File[] externalCacheDirs=ContextCompat.getExternalCacheDirs(context);
    if(externalCacheDirs==null||externalCacheDirs.length==0)
      return result;
    if(externalCacheDirs.length==1)
      {
      if(externalCacheDirs[0]==null)
        return result;
      final String storageState=EnvironmentCompat.getStorageState(externalCacheDirs[0]);
      if(!Environment.MEDIA_MOUNTED.equals(storageState))
        return result;
      if(!includePrimaryExternalStorage&&VERSION.SDK_INT>=VERSION_CODES.HONEYCOMB&&Environment.isExternalStorageEmulated())
        return result;
      }
    if(includePrimaryExternalStorage||externalCacheDirs.length==1)
      {
      if(primaryExternalStorageDirectory!=null)
        result.add(primaryExternalStorageDirectory.getAbsolutePath());
      else
        result.add(getRootOfInnerSdCardFolder(externalCacheDirs[0]));
      }
    for(int i=1;i<externalCacheDirs.length;++i)
      {
      final File file=externalCacheDirs[i];
      if(file==null)
        continue;
      final String storageState=EnvironmentCompat.getStorageState(file);
      if(Environment.MEDIA_MOUNTED.equals(storageState))
        result.add(getRootOfInnerSdCardFolder(externalCacheDirs[i]));
      }
    return result;
    }


private static String getRootOfInnerSdCardFolder(File file)
  {
  if(file==null)
    return null;
  final long totalSpace=file.getTotalSpace();
  while(true)
    {
    final File parentFile=file.getParentFile();
    if(parentFile==null||parentFile.getTotalSpace()!=totalSpace)
      return file.getAbsolutePath();
    file=parentFile;
    }
  }

Вот как я проверяю, что может сделать Uris:

final List<UriPermission> persistedUriPermissions=getContentResolver().getPersistedUriPermissions();

Вот как получить доступ к SD-картам:

startActivityForResult(new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE),42);

public void onActivityResult(int requestCode,int resultCode,Intent resultData)
  {
  if(resultCode!=RESULT_OK)
    return;
  Uri treeUri=resultData.getData();
  DocumentFile pickedDir=DocumentFile.fromTreeUri(this,treeUri);
  grantUriPermission(getPackageName(),treeUri,Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  getContentResolver().takePersistableUriPermission(treeUri,Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  }

Вопросы

  • Можно ли проверить, доступны ли текущие SD-карты, а какие нет, и каким-то образом попросить пользователя получить разрешение на них?

  • Есть ли официальный способ конвертировать между DocumentFile uris и реальными путями? Я нашел этот ответ, но он разбился в моем случае, плюс он выглядит hack-y.

  • Можно ли запросить разрешение у пользователя о конкретном пути? Может быть, даже показать только диалог "вы принимаете" да/нет? "?

  • Можно ли использовать обычный API файлов вместо API DocumentFile после предоставления разрешения?

  • Учитывая путь к файлу/файлу, можно ли просто запросить разрешение на доступ к нему (и проверить, было ли оно задано раньше) или его корневой путь?

  • Можно ли сделать эмулятор SD-картой? В настоящее время упоминается "SD-карта", но она работает как основное внешнее хранилище, и я бы хотел протестировать ее с использованием вторичного внешнего хранилища, чтобы попробовать и использовать новый API.

Я думаю, что для некоторых из этих вопросов один помогает многому ответить другому.

4b9b3361

Ответ 1

Можно ли проверить, доступны ли текущие SD-карты, а какие нет, и как-то попросить пользователя получить разрешение на них?

Есть три части: обнаружение, какие карты есть, проверка наличия карты и запрос доступа.

Если производители устройств являются хорошими парнями, вы можете получить список "внешнего" хранилища, вызвав getExternalFiles с аргументом null ( см. соответствующий javadoc).

Они могут быть не хорошими парнями. И не у всех есть новейшая, самая горячая, безошибочная версия ОС с регулярными обновлениями. Таким образом, могут быть некоторые каталоги, которые там не указаны (например, хранилище OTG USB и т.д.). Если у вас есть сомнения, вы можете получить полный список OS-монстров из файла /proc/self/mounts. Этот файл является частью ядра Linux, этот формат документирован здесь. Вы можете использовать содержимое /proc/self/mounts в качестве резервной копии: проанализировать его, найти используемые файловые системы (fat, ext3, ext4 и т.д.) И удалить дубликаты с выводом getExternalFilesDirs. Предложите оставшиеся опции для пользователей под некоторым ненавязчивым именем, что-то вроде "разных справочников". Это даст вам все возможное внешнее, внутреннее, любое хранилище.


РЕДАКТИРОВАТЬ: после предоставления рекомендации выше я, наконец, попытался следовать за ним сам. До сих пор он работал нормально, но будьте осторожны, но вместо /proc/self/mounts вам лучше разобрать /pros/self/mountinfo (первый из них по-прежнему доступен в современной Linux, но позже - более эффективная и надежная замена). Также учитывайте проблемы атомарности при допущениях, основанных на содержимом списка монтирования.


Вы можете наивно проверить, если каталог доступен для чтения/записи, вызывая canRead и canWrite. Если это удастся, нет смысла делать дополнительную работу. Если это не так, у вас либо есть постоянное разрешение Uri, либо нет.

"Получение разрешения" - уродливая часть. AFAIK, нет никакого способа сделать это чисто в инфраструктуре SAF. Intent.ACTION_PICK звучит как нечто, что может сработать (поскольку он принимает Ури, из которого следует выбирать), но это не так. Возможно, это можно считать ошибкой и должно быть сообщено в Android-трекер ошибок как таковой.

Учитывая путь к файлу/файлу, можно ли просто запросить разрешение на доступ к нему (и проверить, было ли оно задано раньше) или его корневой путь?

Это для ACTION_PICK. Опять же, сборщик SAF не поддерживает ACTION_PICK из коробки. Сторонние файловые менеджеры могут, но очень немногие из них фактически предоставят вам реальный доступ. Вы можете сообщить об этом как об ошибке, если вам это нравится.


EDIT: этот ответ был написан до выхода Android Nougat. Начиная с API 24, по-прежнему невозможно запросить мелкий доступ к определенному каталогу, но по крайней мере вы можете динамически запрашивать доступ ко всему тому: определить том, содержащий файл, и запрос доступа с помощью getAccessIntent с аргументом null (для вторичных томов) или путем запроса WRITE_EXTERNAL_STORAGE (для основного тома).


Есть ли официальный способ конвертировать между DocumentFile uris и реальными путями? Я нашел этот ответ, но он разбился в моем случае, плюс он выглядит взломанным.

Нет. Никогда. Вы навсегда останетесь подчиненными капризам разработчиков Access Access Framework! злой смех

На самом деле существует гораздо более простой способ: просто откройте Uri и проверьте расположение файловой системы созданного дескриптора (для простоты, только версия Lollipop):

public String getFilesystemPath(Context context, Uri uri) {
  ContentResolver res = context.getContentResolver();

  String resolved;
  try (ParcelFileDescriptor fd = res.openFileDescriptor(someSafUri, "r")) {
    final File procfsFdFile = new File("/proc/self/fd/" + fd.getFd());

    resolved = Os.readlink(procfsFdFile.getAbsolutePath());

    if (TextUtils.isEmpty(resolved)
          || resolved.charAt(0) != '/'
          || resolved.startsWith("/proc/")
          || resolved.startsWith("/fd/"))
    return null;
  } catch (Exception errnoe) {
    return null;
  }
}

Если приведенный выше метод возвращает местоположение, вам все равно нужен доступ, чтобы использовать его с помощью File. Если он не возвращает местоположение, рассматриваемый Uri не относится к файлу (даже временному). Это может быть сетевой поток, Unix-канал, что угодно. Вы можете получить версию метода выше для более старых версий Android из этого ответа. Он работает с любым Uri, любым ContentProvider, а не только с SAF, если Uri можно открыть с помощью openFileDescriptor (например, от Intent.CATEGORY_OPENABLE).

Обратите внимание: этот подход выше можно считать официальным, если вы рассматриваете какую-либо часть официального Linux API как такового. Многие программы Linux используют его, и я также видел, что он используется некоторым кодом AOSP (например, в тестах Launcher3).


ИЗМЕНИТЬ: Android Nougat внедрил число изменений безопасности, в первую очередь изменив права доступа к приватным каталогам приложения. Это означает, что, поскольку API 24, вышеприведенный фрагмент будет всегда терпеть неудачу с Исключением, когда Uri ссылается на файл внутри частной папки приложения. Это так, как предполагалось: вы больше не должны знать этот путь вообще. Даже если вы каким-то образом определите путь к файловой системе другими способами, вы не сможете получить доступ к файлам, используя этот путь. Даже если другое приложение взаимодействует с вами и изменяет разрешение файла на читаемый мир, вы все равно не сможете получить к нему доступ. Это связано с тем, что Linux не разрешает доступ к файлам, если у вас нет доступа для поиска к одному из каталогов в пути. Таким образом, получение дескриптора файла из ContentProvider является единственным способом доступа к ним.


Можно ли использовать обычный API файлов вместо API DocumentFile после предоставления разрешения?

Вы не можете. Не по крайней мере на Нуге. Нормальный файл API переходит в ядро ​​Linux для разрешений. Согласно ядру Linux, ваша внешняя SD-карта имеет ограничительные разрешения, которые не позволяют вашему приложению использовать ее. Разрешения, предоставляемые Storage Access Framework, управляются SAF (IIRC, хранящимся в некоторых файлах xml), и ядро ​​ничего не знает о них. Чтобы получить доступ к внешнему хранилищу, вы должны использовать промежуточную сторону (Storage Access Framework). Обратите внимание, что в ядре Linux есть собственный механизм управления доступом к поддеревьям каталога (он называется bind-mounts), но создатели среды доступа к хранилищу либо не знаю об этом или не хочу его использовать.

Вы можете получить некоторую степень доступа (почти все, что можно сделать с помощью File), используя файловый дескриптор, созданный из Uri. Я рекомендую вам прочитать этот ответ, он может иметь некоторую полезную информацию об использовании дескрипторов файлов в целом и в отношении Android.

Ответ 2

Это ограничивает доступ к обычным путям, поскольку я не могу найти способ конвертировать Uris в пути и наоборот (плюс это довольно раздражает). Это также означает, что я не знаю, как проверить, доступны ли текущие SD-карты, поэтому я не знаю, когда спросить у пользователя разрешение на чтение/запись на него (или на них).

Начиная с API 19 (KitKat) непубличный класс Android StorageVolume может использоваться для получения ответов на ваш вопрос с помощью рефлексии:

public static Map<String, String> getSecondaryMountedVolumesMap(Context context) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    Object[] volumes;

    StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
    Method getVolumeListMethod = sm.getClass().getMethod("getVolumeList");
    volumes = (Object[])getVolumeListMethod.invoke(sm);

    Map<String, String> volumesMap = new HashMap<>();
    for (Object volume : volumes) {
        Method getStateMethod = volume.getClass().getMethod("getState");
        String mState = (String) getStateMethod.invoke(volume);

        Method isPrimaryMethod = volume.getClass().getMethod("isPrimary");
        boolean mPrimary = (Boolean) isPrimaryMethod.invoke(volume);

        if (!mPrimary && mState.equals("mounted")) {
            Method getPathMethod = volume.getClass().getMethod("getPath");
            String mPath = (String) getPathMethod.invoke(volume);

            Method getUuidMethod = volume.getClass().getMethod("getUuid");
            String mUuid = (String) getUuidMethod.invoke(volume);

            if (mUuid != null && mPath != null)
                volumesMap.put(mUuid, mPath);
        }
    }
    return volumesMap;
}

Этот метод дает вам все смонтированные неосновные хранилища (включая USB OTG) и отображает их UUID на их путь, который поможет вам преобразовать DocumentUri в реальный путь (и наоборот):

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static String convertToPath(Uri treeUri, Context context) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    String documentId = DocumentsContract.getTreeDocumentId(treeUri);
    if (documentId != null){
        String[] split = documentId.split(":");
        String uuid = null;
        if (split.length > 0)
            uuid = split[0];
        String pathToVolume = null;
        Map<String, String> volumesMap = getSecondaryMountedVolumesMap(context);
        if (volumesMap != null && uuid != null)
            pathToVolume = volumesMap.get(uuid);
        if (pathToVolume != null) {
            String pathInsideOfVolume = split.length == 2 ? IFile.SEPARATOR + split[1] : "";
            return pathToVolume + pathInsideOfVolume;
        }
    }
    return null;
}

Кажется, Google не собирается давать нам лучший способ справиться со своим уродливым SAF.

EDIT: Кажется, этот подход бесполезен в Android 6.0...