program story

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

inputbox 2020. 7. 27. 07:55
반응형

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


배경

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

Android Lollipop (5.0)부터 개발자는 이 Google 그룹 게시물 에 작성된대로 사용자에게 특정 폴더에 대한 액세스를 허용하도록 요청하는 새로운 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 카드를 다른 카드로 교체하면 어떻게됩니까?

좋은 질문이 많이 있습니다.

어떻게 사용합니까?

KitKat의 Storage Access Framework와 상호 작용하기위한 훌륭한 자습서는 다음과 같습니다.

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를 시도하는 방법이 있습니까?

그러나 기본 공유 저장 장치는 에뮬레이터에서도 선택기에 나타납니다. 앱이 공유 스토리지에 액세스하기 위해 Storage Access Framework 만 사용하는 경우 더 이상 READ/WRITE_EXTERNAL_STORAGE권한이 필요하지 않으며 권한 제거하거나이 android:maxSdkVersion기능을 사용하여 이전 플랫폼 버전에서만 요청할 수 있습니다.

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

실제 미디어가 포함 된 경우 기본 미디어의 UUID (예 : FAT 일련 번호)는 항상 반환 된 Uri에 레코딩됩니다. 사용자가 여러 슬롯 사이에서 미디어를 교체하더라도 시스템은이를 사용하여 사용자가 원래 선택한 미디어에 연결합니다.

사용자가 두 번째 카드를 교환하면 새 카드에 액세스하라는 메시지가 표시됩니다. 시스템은 UUID 단위로 보조금을 기억하므로 사용자가 나중에 다시 삽입하면 원래 카드에 대한 액세스 권한이 계속 부여됩니다.

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


아래 링크 된 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);
}

Storage Access Framework의 응답 처리 :

@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


It is just a complementary answer.

After you created a new file, you might need to save its location into your database and read it tomorrow. You can read retrieve it again using this method:

/**
 * 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();
    }
}

참고URL : https://stackoverflow.com/questions/26744842/how-to-use-the-new-sd-card-access-api-presented-for-android-5-0-lollipop

반응형