android - 새 Lollipop API를 사용하여 모든 SD 카드에 액세스하는 방법





file uri android-sdcard documentfile (3)


현재의 SD 카드에 액세스 할 수 있는지 확인하는 것이 가능합니까? 그렇지 않은 경우 사용자에게 허가를 받도록 요청할 수 있습니까?

이 부분에는 3 가지 부분이 있습니다. 즉, 감지하고있는 카드, 카드가 장착되어 있는지 확인하고 액세스를 요청하는 것입니다.

디바이스 메이커가 좋은 분이라면, 인수가 null getExternalFiles 를 호출 해, 「외부」스토리지의리스트를 취득 할 수 있습니다 (관련 javadoc를 참조).

그들은 좋은 사람들이 아닐 수도 있습니다. 그리고 모든 사람들이 정기적으로 업데이트되는 가장 새롭고, 가장 뜨겁고, 버그가없는 OS 버전을 가지고있는 것은 아닙니다. 따라서 목록에없는 일부 디렉토리 (예 : OTG USB 저장소 등)가있을 수 있습니다. 의심 스러울 때마다 /proc/self/mounts 파일에서 전체 OS 마운트 목록을 가져올 수 있습니다. 이 파일은 리눅스 커널의 일부이며, 형식은 here 에 문서화되어 here . /proc/self/mounts 을 백업으로 사용할 수 있습니다 : 구문 분석, 사용 가능한 파일 시스템 (fat, ext3, ext4 등)을 찾고 getExternalFilesDirs 출력으로 중복을 제거하십시오. "misc directory"와 같은 다소 복잡한 이름으로 남은 옵션을 사용자에게 제공하십시오. 그렇게하면 모든 외부 저장 장치, 내부 저장 장치를 사용할 수 있습니다.

편집 : 위의 조언을 준 후 드디어 나 자신을 따르려고했습니다. 지금까지는 괜찮 았지만 /proc/self/mounts 대신 parsing /pros/self/mountinfo 하는 것이 /pros/self/mountinfo (이전 버전은 현대 Linux에서 여전히 사용할 수 있지만 나중에 더 강력하고 나은 대체 방법입니다). 또한 마운트 목록의 내용을 기반으로 가정을 만들 때 원 자성 문제 를 고려해야합니다.

디렉토리가 canReadcanWrite 를 호출하여 읽기 / 쓰기가 가능한지 여부를 순진하게 확인할 수 있습니다. 이것이 성공하면 추가 작업을 할 필요가 없습니다. 그렇지 않으면 Uri 권한이 지속되거나 실행되지 않습니다.

"허가 받기"는 추악한 부분입니다. AFAIK, SAF 인프라 내에서이를 깨끗하게 처리 할 방법이 없습니다. Intent.ACTION_PICK 작동 할 수있는 것과 비슷하게 들립니다 (Uri를 받아 들일 있기 때문에 선택해야 함). 그러나 그렇지 않습니다. 어쩌면 이것은 버그로 간주 될 수 있으며 Android 버그 추적기에보고되어야합니다.

파일 / 파일 경로가 주어지면 액세스 권한 (이전에 부여되었는지 확인)이나 루트 경로를 요청할 수 있습니까?

이것이 ACTION_PICK 목적입니다. SAF picker는 ACTION_PICK out of box를 지원하지 않습니다. 제 3 자 파일 관리자는 실제로 액세스 할 수있는 사용자는 거의 없을 수 있습니다. 기분이 좋다면 이것을 버그로보고 할 수도 있습니다.

수정 :이 대답은 안드로이드 누가가 나오기 전에 작성되었습니다. API 24에서 특정 디렉토리에 대한 세분화 된 액세스를 요청하는 것은 여전히 ​​불가능하지만 적어도 전체 볼륨에 대한 액세스를 동적으로 요청할 수 있습니다 : 볼륨 을 포함하여 파일을 포함하고 null 인수와 함께 getAccessIntent 를 사용하여 액세스를 요청하십시오 (2 차 볼륨의 경우) 또는 WRITE_EXTERNAL_STORAGE 권한 (기본 볼륨의 경우)을 요청합니다.

DocumentFile uris와 실제 경로를 변환하는 공식적인 방법이 있습니까? 이 답변을 찾았지만 내 경우에는 추락했습니다. 게다가 해킹으로 보입니다.

안돼. 당신은 영원히 스토리지 액세스 프레임 워크 크리에이터의 변덕에 종속되어 있습니다! 사악한 웃음

사실 훨씬 간단한 방법이 있습니다 : 단지 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는 파일 ​​(심지어 임시 파일)을 참조하지 않습니다. 그것은 네트워크 스트림, 유닉스 파이프 일 수도 있습니다. 이 대답 을 통해 이전 Android 버전에 대한 위의 메소드 버전을 얻을 수 있습니다. Uri이 openFileDescriptor (예 : Intent.CATEGORY_OPENABLE 에서 Intent.CATEGORY_OPENABLE )로 열 수있는 한 모든 Uri, SAF뿐만 아니라 모든 ContentProvider에서 Intent.CATEGORY_OPENABLE 합니다.

공식 Linux API의 일부로 간주하면 위의 접근 방식은 공식적인 것으로 간주 될 수 있습니다. 많은 Linux 소프트웨어가이를 사용하며, Launcher3 테스트와 같은 일부 AOSP 코드에서도이 소프트웨어를 사용하는 것으로 나타났습니다.

수정 : 안드로이드 누갓은 보안 변경 사항의 번호를 도입, 특히 응용 프로그램 개인 디렉터리의 사용 권한을 변경합니다. 즉, Uri가 응용 프로그램 개인 디렉토리 내의 파일을 참조 할 때 API 24부터 위의 스니 j은 예외로 항상 실패합니다. 이것은 의도 한 것입니다. 더 이상 그 경로를 전혀 알지 못할 것입니다 . 어떤 방법 으로든 다른 방법으로 파일 시스템 경로를 결정하더라도 해당 경로를 사용하여 파일에 액세스 할 수 없습니다. 다른 응용 프로그램이 사용자와 협력하여 파일의 권한을 세계에서 읽을 수있는 것으로 변경하더라도 여전히 액세스 할 수 없습니다. 이것은 경로에있는 디렉토리 중 하나에 대한 검색 권한이없는 경우 Linux가 파일에 대한 액세스를 허용하지 않기 때문입니다. 따라서 ContentProvider에서 파일 설명자를 수신하는 것이 유일한 방법입니다.

권한이 부여되면 DocumentFile API 대신 일반 File API를 사용할 수 있습니까?

당신은 할 수 없습니다. 적어도 Nougat에는 없습니다. 일반 파일 API는 권한을 얻기 위해 리눅스 커널로 간다. Linux 커널에 따르면 외부 SD 카드에는 제한적인 권한이있어 앱이이를 사용하지 못하게합니다. 스토리지 액세스 프레임 워크에서 제공하는 권한은 SAF (일부 XML 파일에 저장된 IIRC)에서 관리하며 커널은 이에 대해 아무것도 모릅니다. 중간 저장 장치 (저장소 액세스 프레임 워크)를 사용하여 외부 저장소에 액세스해야합니다. Linux 커널에는 디렉토리 하위 트리 ( bind-mounts 라고 함)에 대한 액세스를 관리하는 자체 메커니즘이 있지만 Storage Access Framework 작성자는이를 인식하지 못하거나 사용하고 싶지 않습니다.

Uri에서 만든 파일 설명자를 사용하여 어느 정도의 액세스 ( File 로 수행 할 수있는 거의 모든 작업)를 수행 할 수 있습니다. 이 답변 을 읽으시 길 권장합니다. 일반적으로 안드로이드와 관련하여 파일 디 스크립트를 사용하는 것에 대한 유용한 정보가있을 수 있습니다.

배경

Lollipop부터는 앱에서 실제 SD 카드에 액세스 할 수 있습니다 (Kitkat에서 액세스 할 수 없으며 공식적으로 지원되지 않았지만 이전 버전에서 지원됨).

문제

SD 카드를 지원하는 Lollipop 장치를 보는 것은 매우 드물기 때문에 에뮬레이터에 SD 카드 지원을 에뮬레이션 할 수있는 기능이 없기 때문에 테스트하는 데 꽤 오래 걸렸습니다. .

어쨌든 일반 파일 클래스를 사용하여 SD 카드에 액세스하는 대신 (일단 권한을 얻었 으면) DocumentFile을 사용하여 Uris를 사용해야합니다.

이것은 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);
  }

질문들

  1. 현재의 SD 카드에 액세스 할 수 있는지 확인하는 것이 가능합니까? 그렇지 않은 경우 사용자에게 허가를 받도록 요청할 수 있습니까?

  2. DocumentFile uris와 실제 경로를 변환하는 공식적인 방법이 있습니까? 이 답변을 찾았지만 내 경우에는 추락했습니다. 게다가 해킹으로 보입니다.

  3. 특정 경로에 대한 사용자의 허가를 요청할 수 있습니까? 어쩌면 "예 / 아니오를 받아들나요?"라는 대화 만 표시 할 수도 있습니다.

  4. 권한이 부여되면 DocumentFile API 대신 일반 File API를 사용할 수 있습니까?

  5. 파일 / 파일 경로가 주어지면 액세스 권한 (이전에 부여되었는지 확인)이나 루트 경로를 요청할 수 있습니까?

  6. 에뮬레이터에 SD 카드를 만들 수 있습니까? 현재 "SD 카드"가 언급되어 있지만 기본 외부 저장소로 작동하며 새로운 API를 사용해 보려고 보조 외부 저장소를 사용하여 테스트하고 싶습니다.

그 질문 중 일부는 다른 질문에 답하는 데 많은 도움이된다고 생각합니다.




이것은 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를 포함하여 마운트되지 않은 모든 기본 저장소를 제공하고 DocumentUri를 실제 경로로 변환하는 데 도움이되는 경로에 UUID를 매핑합니다.

@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를 처리하는 데 더 좋은 방법을 제공하지 않습니다.

편집 : 이 접근법 안드로이드 6.0 쓸모없는 것 같아요 ...




두 애플리케이션의 서명이 같으면 (즉, 두 APPS가 모두 사용자이고 동일한 키로 서명 된 것임), 다음과 같이 다른 앱 활동을 호출 할 수 있습니다.

Intent LaunchIntent = getActivity().getPackageManager().getLaunchIntentForPackage(CALC_PACKAGE_NAME);
startActivity(LaunchIntent);

희망이 도움이됩니다.





android file uri android-sdcard documentfile