Android 5.0 (Lollipop) 용으로 제공되는 새로운 SD 카드 액세스 API를 사용하는 방법은 무엇입니까?


118

배경

안드로이드 4.4 (킷캣), 구글은 매우 제한 SD 카드에 대한 액세스를했다.

Android Lollipop (5.0)부터 개발자는 이 Google-Groups 게시물 에 작성된대로 사용자에게 특정 폴더에 대한 액세스를 허용할지 확인하는 새 API를 사용할 수 있습니다 .

문제

이 게시물은 다음 두 웹 사이트를 방문하도록 안내합니다.

이것은 내부 예제처럼 보이지만 (아마도 나중에 API 데모에 표시 될 것입니다), 무슨 일이 일어나고 있는지 이해하기가 매우 어렵습니다.

이것은 새 API의 공식 문서이지만 사용 방법에 대한 자세한 내용은 설명하지 않습니다.

다음과 같이 알려줍니다.

문서의 전체 하위 트리에 대한 전체 액세스 권한이 필요한 경우 먼저 ACTION_OPEN_DOCUMENT_TREE를 실행하여 사용자가 디렉토리를 선택할 수 있도록합니다. 그런 다음 결과 getData ()를 fromTreeUri (Context, Uri)에 전달하여 사용자가 선택한 트리 작업을 시작합니다.

DocumentFile 인스턴스의 트리를 탐색 할 때 항상 getUri ()를 사용하여 해당 객체의 기본 문서를 나타내는 Uri를 가져 와서 openInputStream (Uri) 등과 함께 사용할 수 있습니다.

KITKAT 또는 이전 버전을 실행하는 장치에서 코드를 단순화하려면 DocumentsProvider의 동작을 에뮬레이트하는 fromFile (File)을 사용할 수 있습니다.

질문

새 API에 대해 몇 가지 질문이 있습니다.

  1. 실제로 어떻게 사용합니까?
  2. 게시물에 따르면 OS는 앱에 파일 / 폴더에 대한 액세스 권한이 부여되었음을 기억합니다. 파일 / 폴더에 액세스 할 수 있는지 어떻게 확인합니까? 액세스 할 수있는 파일 / 폴더 목록을 반환하는 함수가 있습니까?
  3. Kitkat에서이 문제를 어떻게 처리합니까? 지원 라이브러리의 일부입니까?
  4. 어떤 앱이 어떤 파일 / 폴더에 액세스 할 수 있는지 보여주는 설정 화면이 OS에 있습니까?
  5. 동일한 기기에서 여러 사용자를 위해 앱을 설치하면 어떻게 되나요?
  6. 이 새로운 API에 대한 다른 문서 / 튜토리얼이 있습니까?
  7. 권한을 취소 할 수 있습니까? 그렇다면 앱으로 전송되는 인 텐트가 있습니까?
  8. 권한 요청이 선택한 폴더에서 반복적으로 작동합니까?
  9. 권한을 사용하면 사용자가 선택한 여러 항목을 선택할 수 있습니까? 아니면 앱에서 허용 할 파일 / 폴더를 구체적으로 알려야합니까?
  10. 에뮬레이터에서 새 API를 사용해 볼 수있는 방법이 있습니까? 즉, SD 카드 파티션이 있지만 기본 외부 저장소로 작동하므로 모든 액세스 권한이 이미 부여되었습니다 (간단한 권한 사용).
  11. 사용자가 SD 카드를 다른 카드로 교체하면 어떻게됩니까?

FWIW, AndroidPolice는 오늘 이것에 대한 작은 기사 를 가지고 있습니다.
fattire 2014

@fattire 감사합니다. 그러나 그들은 내가 이미 읽은 것을 보여주고 있습니다. 그래도 좋은 소식입니다.
안드로이드 개발자

32
새로운 OS가 나올 때마다, 그들은 우리의 삶을 더 복잡하게
Phantômaxx

@Funkystein 사실입니다. 그들이 Kitkat에서 해냈 으면 좋겠어요. 이제 처리 할 3 가지 유형의 동작이 있습니다.
안드로이드 개발자

1
@Funkystein 모르겠어요. 오래 전에 사용했습니다. 이 수표를하는 것은 그렇게 나쁘지 않다고 생각합니다. 당신은 그들도 인간이라는 것을 기억해야합니다. 그래서 그들은 실수를하고 때때로 그들의 마음을 바꿀 수 있습니다.
안드로이드 개발자

답변:


143

많은 좋은 질문, 파헤쳐 보겠습니다. :)

어떻게 사용합니까?

다음은 KitKat의 스토리지 액세스 프레임 워크와 상호 작용하기위한 훌륭한 튜토리얼입니다.

https://developer.android.com/guide/topics/providers/document-provider.html#client

Lollipop에서 새로운 API와 상호 작용하는 것은 매우 유사합니다. 사용자에게 디렉토리 트리를 선택하라는 메시지를 표시하려면 다음과 같은 인 텐트를 시작할 수 있습니다.

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

그런 다음 onActivityResult ()에서 사용자가 선택한 Uri를 새 DocumentFile 도우미 클래스에 전달할 수 있습니다. 다음은 선택한 디렉터리의 파일을 나열한 다음 새 파일을 만드는 간단한 예제입니다.

public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
    if (resultCode == RESULT_OK) {
        Uri treeUri = resultData.getData();
        DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);

        // List all existing files inside picked directory
        for (DocumentFile file : pickedDir.listFiles()) {
            Log.d(TAG, "Found file " + file.getName() + " with size " + file.length());
        }

        // Create a new file and write into it
        DocumentFile newFile = pickedDir.createFile("text/plain", "My Novel");
        OutputStream out = getContentResolver().openOutputStream(newFile.getUri());
        out.write("A long time ago...".getBytes());
        out.close();
    }
}

에서 반환 된 Uri DocumentFile.getUri()는 다양한 플랫폼 API에서 사용할 수있을만큼 유연합니다. 예를 들어, 사용하여 공유 할 수 Intent.setData()와 함께 Intent.FLAG_GRANT_READ_URI_PERMISSION.

당신은 열린 우리당이 네이티브 코드에서, 당신이 호출 할 수있는 액세스하려면 ContentResolver.openFileDescriptor()다음 사용 ParcelFileDescriptor.getFd()또는 detachFd()전통적인 POSIX 파일 기술자의 정수를 얻을.

파일 / 폴더에 액세스 할 수 있는지 어떻게 확인합니까?

기본적으로 Storage Access Frameworks 인 텐트를 통해 반환 된 Uris는 재부팅시 유지 되지 않습니다 . 플랫폼은 권한을 유지할 수있는 기능을 "제공"하지만 원하는 경우 권한을 "취득"해야합니다. 위의 예에서 다음을 호출합니다.

    getContentResolver().takePersistableUriPermission(treeUri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION |
            Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

ContentResolver.getPersistedUriPermissions()API 를 통해 앱이 액세스 할 수있는 지속 된 권한을 항상 파악할 수 있습니다 . 지속 된 Uri에 더 이상 액세스 할 필요가없는 경우 ContentResolver.releasePersistableUriPermission().

KitKat에서 사용할 수 있습니까?

아니요, 플랫폼의 이전 버전에 새 기능을 소급하여 추가 할 수 없습니다.

파일 / 폴더에 액세스 할 수있는 앱을 볼 수 있습니까?

현재이를 표시하는 UI는 없지만 adb shell dumpsys activity providers출력 의 "허용 된 Uri 권한"섹션에서 세부 사항을 찾을 수 있습니다 .

동일한 기기에서 여러 사용자를 위해 앱을 설치하면 어떻게 되나요?

Uri 권한 부여는 다른 모든 다중 사용자 플랫폼 기능과 마찬가지로 사용자별로 격리됩니다. 즉, 두 명의 서로 다른 사용자에서 실행되는 동일한 앱에는 중복되거나 공유 된 Uri 권한 부여가 없습니다.

권한을 취소 할 수 있습니까?

지원 DocumentProvider는 클라우드 기반 문서가 삭제되는 경우와 같이 언제든지 권한을 취소 할 수 있습니다. 이러한 취소 된 권한을 발견하는 가장 일반적인 방법은 ContentResolver.getPersistedUriPermissions()위에서 언급 한 권한이 사라지는 것 입니다.

권한은 부여와 관련된 앱에 대한 앱 데이터가 지워질 때마다 취소됩니다.

권한 요청이 선택한 폴더에서 반복적으로 작동합니까?

네,이 인 ACTION_OPEN_DOCUMENT_TREE텐트는 기존 파일과 새로 생성 된 파일 및 디렉토리에 대한 재귀 적 액세스를 제공합니다.

다중 선택이 가능합니까?

네, KitKat 이후로 다중 선택이 지원되었으며 의도를 EXTRA_ALLOW_MULTIPLE시작할 때 설정하여 허용 할 수 있습니다 ACTION_OPEN_DOCUMENT. 당신은 사용할 수 있습니다 Intent.setType()또는 EXTRA_MIME_TYPES포착 할 수있는 파일의 종류를 좁힐 수 :

http://developer.android.com/reference/android/content/Intent.html#ACTION_OPEN_DOCUMENT

에뮬레이터에서 새 API를 사용해 볼 수있는 방법이 있습니까?

예, 기본 공유 저장 장치는 에뮬레이터에서도 선택기에 표시되어야합니다. 앱이 공유 저장소에 액세스하기 위해 저장소 액세스 프레임 워크 만 사용하는 경우 더 이상 READ/WRITE_EXTERNAL_STORAGE권한이 필요하지 않으며 권한 제거하거나 android:maxSdkVersion기능을 사용하여 이전 플랫폼 버전에서만 요청할 수 있습니다.

사용자가 SD 카드를 다른 카드로 교체하면 어떻게됩니까?

물리적 미디어가 관련되면 기본 미디어의 UUID (예 : FAT 일련 번호)가 항상 반환 된 Uri에 기록됩니다. 시스템은이를 사용하여 사용자가 여러 슬롯 사이에서 미디어를 교체하더라도 사용자가 원래 선택한 미디어에 연결합니다.

사용자가 두 번째 카드를 교체하는 경우 새 카드에 액세스 할 것인지 묻는 메시지가 표시되어야합니다. 시스템은 UUID 단위로 허가를 기억하므로 사용자가 나중에 다시 삽입하는 경우 이전에 원래 카드에 대한 액세스 권한을 계속 부여 할 수 있습니다.

http://en.wikipedia.org/wiki/Volume_serial_number


2
내가 참조. 그래서 결정은 (파일의) 알려진 API에 더 많은 것을 추가하는 대신 다른 것을 사용하는 것입니다. 확인. 다른 질문 (첫 번째 의견에 작성)에 답해 주시겠습니까? BTW,이 모든 것에 답 해주셔서 감사합니다.
안드로이드 개발자

7
@JeffSharkey OPEN_DOCUMENT_TREE에 시작 위치 힌트를 제공하는 방법이 있습니까? 사용자가 응용 프로그램이 액세스해야하는 항목을 탐색하는 데 항상 최고는 아닙니다.
Justin

2
마지막으로 수정 된 타임 스탬프 ( 파일 클래스의 setLastModified () 메서드 ) 를 변경하는 방법이 있습니까? 아카이버와 같은 응용 프로그램의 기본입니다.
Quark

1
저장된 폴더 문서 Uri가 있고 나중에 장치 재부팅 후 특정 시점에 파일을 나열하려고한다고 가정 해 보겠습니다. DocumentFile.fromTreeUri는 제공 한 Uri (하위 Uri 포함)에 관계없이 항상 루트 파일을 나열하므로 DocumentFile이 루트 또는 SingleDocument를 나타내지 않는 목록 파일을 호출 할 수있는 DocumentFile을 어떻게 생성합니까?
AndersC 2015 년

1
@JeffSharkey이 URI는 출력 파일 경로로 문자열을 받아들이 기 때문에 MediaMuxer에서 어떻게 사용할 수 있습니까? MediaMuxer (java.lang.String의, INT
Petrakeas

45

아래 링크 된 Github의 Android 프로젝트에서 Android 5의 extSdCard에 쓸 수있는 작업 코드를 찾을 수 있습니다. 사용자가 전체 SD 카드에 대한 액세스 권한을 부여한 다음이 카드의 모든 곳에 쓸 수 있다고 가정합니다. (단일 파일에만 액세스하려면 작업이 더 쉬워집니다.)

메인 코드 스니 플릿

스토리지 액세스 프레임 워크 트리거 :

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void triggerStorageAccessFramework() {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS);
}

스토리지 액세스 프레임 워크의 응답 처리 :

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public final void onActivityResult(final int requestCode, final int resultCode, final Intent resultData) {
    if (requestCode == SettingsFragment.REQUEST_CODE_STORAGE_ACCESS) {
        Uri treeUri = null;
        if (resultCode == Activity.RESULT_OK) {
            // Get Uri from Storage Access Framework.
            treeUri = resultData.getData();

            // Persist URI in shared preference so that you can use it later.
            // Use your own framework here instead of PreferenceUtil.
            PreferenceUtil.setSharedPreferenceUri(R.string.key_internal_uri_extsdcard, treeUri);

            // Persist access permissions.
            final int takeFlags = resultData.getFlags()
                & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            getActivity().getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
        }
    }
}

Storage Access Framework를 통해 파일에 대한 outputStream 가져 오기 (저장된 URL 사용, 외부 SD 카드의 루트 폴더 URL이라고 가정)

DocumentFile targetDocument = getDocumentFile(file, false);
OutputStream outStream = Application.getAppContext().
    getContentResolver().openOutputStream(targetDocument.getUri());

다음 도우미 메서드를 사용합니다.

public static DocumentFile getDocumentFile(final File file, final boolean isDirectory) {
    String baseFolder = getExtSdCardFolder(file);

    if (baseFolder == null) {
        return null;
    }

    String relativePath = null;
    try {
        String fullPath = file.getCanonicalPath();
        relativePath = fullPath.substring(baseFolder.length() + 1);
    }
    catch (IOException e) {
        return null;
    }

    Uri treeUri = PreferenceUtil.getSharedPreferenceUri(R.string.key_internal_uri_extsdcard);

    if (treeUri == null) {
        return null;
    }

    // start with root of SD card and then parse through document tree.
    DocumentFile document = DocumentFile.fromTreeUri(Application.getAppContext(), treeUri);

    String[] parts = relativePath.split("\\/");
    for (int i = 0; i < parts.length; i++) {
        DocumentFile nextDocument = document.findFile(parts[i]);

        if (nextDocument == null) {
            if ((i < parts.length - 1) || isDirectory) {
                nextDocument = document.createDirectory(parts[i]);
            }
            else {
                nextDocument = document.createFile("image", parts[i]);
            }
        }
        document = nextDocument;
    }

    return document;
}

public static String getExtSdCardFolder(final File file) {
    String[] extSdPaths = getExtSdCardPaths();
    try {
        for (int i = 0; i < extSdPaths.length; i++) {
            if (file.getCanonicalPath().startsWith(extSdPaths[i])) {
                return extSdPaths[i];
            }
        }
    }
    catch (IOException e) {
        return null;
    }
    return null;
}

/**
 * Get a list of external SD card paths. (Kitkat or higher.)
 *
 * @return A list of external SD card paths.
 */
@TargetApi(Build.VERSION_CODES.KITKAT)
private static String[] getExtSdCardPaths() {
    List<String> paths = new ArrayList<>();
    for (File file : Application.getAppContext().getExternalFilesDirs("external")) {
        if (file != null && !file.equals(Application.getAppContext().getExternalFilesDir("external"))) {
            int index = file.getAbsolutePath().lastIndexOf("/Android/data");
            if (index < 0) {
                Log.w(Application.TAG, "Unexpected external file dir: " + file.getAbsolutePath());
            }
            else {
                String path = file.getAbsolutePath().substring(0, index);
                try {
                    path = new File(path).getCanonicalPath();
                }
                catch (IOException e) {
                    // Keep non-canonical path.
                }
                paths.add(path);
            }
        }
    }
    return paths.toArray(new String[paths.size()]);
}

 /**
 * Retrieve the application context.
 *
 * @return The (statically stored) application context
 */
public static Context getAppContext() {
    return Application.mApplication.getApplicationContext();
}

전체 코드에 대한 참조

https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/fragments/SettingsFragment.java#L521

https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/FileUtil.java


1
SD 카드 만 처리하는 최소화 된 프로젝트에 넣어 주시겠습니까? 또한 사용 가능한 모든 외부 저장소에 액세스 할 수 있는지 확인하여 아무것도 권한을 요청하지 않도록 어떻게 할 수 있는지 알고 있습니까?
안드로이드 개발자

1
내가 이것을 더 찬성 할 수 있기를 바랍니다. 이 솔루션의 중간에 있었고 문서 탐색이 너무 끔찍하여 잘못하고 있다고 생각했습니다. 이것에 대해 약간의 확인을 가져서 좋습니다. Google에 감사드립니다.
Anthony

1
예, 외부 SD에 쓰기 위해 더 이상 일반 파일 접근 방식을 사용할 수 없습니다. 반면에 이전 Android 버전 및 기본 SD의 경우 파일이 여전히 가장 효율적인 접근 방식입니다. 따라서 파일 액세스를 위해 사용자 정의 유틸리티 클래스를 사용해야합니다.
Jörg Eisfeld 2015 년

15
@ JörgEisfeld : File254 번 사용하는 앱이 있습니다. 그것을 고치는 것을 상상할 수 있습니까 ?? Android는 이전 버전과의 호환성이 완전히 부족하여 개발자에게 악몽이되고 있습니다. Google이 외부 저장소와 관련하여이 모든 어리석은 결정을 내린 이유를 설명하는 곳을 아직 찾지 못했습니다. 일부는 "보안"이라고 주장하지만 모든 앱이 내부 저장소를 엉망으로 만들 수 있기 때문에 당연히 말도 안됩니다. 제 생각에는 클라우드 서비스를 사용하도록 강제하는 것입니다. 고맙게도로 해결할에게 문제를 응원 안드로이드 ... <6 적어도 ....
루이스 A. Florit

1
확인.
마법처럼

0

그것은 단지 보완적인 대답 일뿐입니다.

새 파일을 만든 후 해당 위치를 데이터베이스에 저장하고 내일 읽어야 할 수 있습니다. 다음 방법을 사용하여 다시 검색 할 수 있습니다.

/**
 * Get {@link DocumentFile} object from SD card.
 * @param directory SD card ID followed by directory name, for example {@code 6881-2249:Download/Archive},
 *                 where ID for SD card is {@code 6881-2249}
 * @param fileName for example {@code intel_haxm.zip}
 * @return <code>null</code> if does not exist
 */
public static DocumentFile getExternalFile(Context context, String directory, String fileName){
    Uri uri = Uri.parse("content://com.android.externalstorage.documents/tree/" + directory);
    DocumentFile parent = DocumentFile.fromTreeUri(context, uri);
    return parent != null ? parent.findFile(fileName) : null;
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == SettingsFragment.REQUEST_CODE_STORAGE_ACCESS && resultCode == RESULT_OK) {
        int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
        getContentResolver().takePersistableUriPermission(data.getData(), takeFlags);
        String sdcard = data.getDataString().replace("content://com.android.externalstorage.documents/tree/", "");
        try {
            sdcard = URLDecoder.decode(sdcard, "ISO-8859-1");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        // for example, sdcardId results "6312-2234"
        String sdcardId = sdcard.substring(0, sdcard.indexOf(':'));
        // save to preferences if you want to use it later
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        preferences.edit().putString("sdcard", sdcardId).apply();
    }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.