Google은 흑인 공동체를 위한 인종 간 평등을 진전시키기 위해 노력하고 있습니다. Google에서 어떤 노력을 하고 있는지 확인하세요.

공유 저장소의 미디어 파일에 액세스

많은 앱에서 더욱 풍부한 사용자 환경을 제공하기 위해 사용자가 외부 저장소 볼륨에서 사용 가능한 미디어를 제공하고 액세스할 수 있게 합니다. 프레임워크는 미디어 저장소라고 하는 미디어 컬렉션에 최적화된 색인을 제공하여 미디어 파일을 더욱 쉽게 검색하고 업데이트할 수 있게 합니다. 앱이 제거된 이후에도 이러한 파일은 사용자 기기에 남아 있습니다.

미디어 저장소 추상화와 상호작용하려면 다음과 같이 앱 컨텍스트에서 검색한 ContentResolver 객체를 사용합니다.

Kotlin

val projection = arrayOf(media-database-columns-to-retrieve)
val selection = sql-where-clause-with-placeholder-variables
val selectionArgs = values-of-placeholder-variables
val sortOrder = sql-order-by-clause

applicationContext.contentResolver.query(
    MediaStore.media-type.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder
)?.use { cursor ->
    while (cursor.moveToNext()) {
        // Use an ID column from the projection to get
        // a URI representing the media item itself.
    }
}

자바

String[] projection = new String[] {
        media-database-columns-to-retrieve
};
String selection = sql-where-clause-with-placeholder-variables;
String[] selectionArgs = new String[] {
        values-of-placeholder-variables
};
String sortOrder = sql-order-by-clause;

Cursor cursor = getApplicationContext().getContentResolver().query(
    MediaStore.media-type.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder
);

while (cursor.moveToNext()) {
    // Use an ID column from the projection to get
    // a URI representing the media item itself.
}

시스템은 자동으로 외부 저장소 볼륨을 검색하고 다음과 같이 잘 정의된 컬렉션에 미디어 파일을 추가합니다.

  • 이미지 - 사진 및 스크린샷을 포함하며, DCIM/Pictures/ 디렉터리에 저장됩니다. 시스템은 이러한 파일을 MediaStore.Images 테이블에 추가합니다.
  • 동영상 - DCIM/, Movies/Pictures/ 디렉터리에 저장됩니다. 시스템은 이러한 파일을 MediaStore.Video 테이블에 추가합니다.
  • 오디오 파일 - Alarms/, Audiobooks/, Music/, Notifications/, Podcasts/Ringtones/ 디렉터리에 저장된 오디오 파일과 Music/ 또는 Movies/ 디렉터리에 있는 오디오 재생목록이 포함됩니다. 시스템은 이러한 파일을 MediaStore.Audio 테이블에 추가합니다.
  • 다운로드한 파일 - Download/ 디렉터리에 저장됩니다. Android 10(API 수준 29) 이상을 실행하는 기기에서는 이러한 파일이 MediaStore.Downloads 테이블에 저장됩니다. Android 9(API 수준 28) 이하에서는 이 테이블을 사용할 수 없습니다.

미디어 저장소에는 MediaStore.Files라는 컬렉션도 포함되어 있습니다. 컬렉션의 콘텐츠는 앱이 Android 10 이상을 타겟팅하는 앱에서 사용 가능한 범위 지정 저장소를 사용하는지 여부에 따라 달라집니다.

  • 범위 지정 저장소를 사용 설정하면 컬렉션에는 앱에서 생성한 사진, 동영상 및 오디오 파일만 표시됩니다.
  • 범위 지정 저장소를 사용할 수 없거나 사용하고 있지 않다면 컬렉션에는 모든 유형의 미디어 파일이 표시됩니다.

필요한 권한 요청

미디어 파일에 관한 작업을 실행하기 전에 앱이 이러한 파일에 액세스하는 데 필요한 권한을 선언했는지 확인해야 합니다. 그러나 앱에서 필요하지 않거나 사용하지 않는 권한을 선언해서는 안 됩니다.

저장소 권한

앱에서 미디어 파일에 액세스하기 위한 권한 모델은 앱이 Android 10 이상을 타겟팅하는 앱에서 사용 가능한 범위 지정 저장소를 사용하는지 여부에 따라 달라집니다.

범위 지정 저장소 사용 설정됨

앱이 범위 지정 저장소를 사용하면 Android 9(API 수준 28) 이하를 실행하는 기기에 한해서만 저장소 관련 권한을 요청해야 합니다. 다음과 같이 앱 매니페스트 파일의 권한 선언에 android:maxSdkVersion 속성을 추가하여 이 조건을 적용할 수 있습니다.

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                 android:maxSdkVersion="28" />

Android 10 이상을 실행하는 기기의 저장소 관련 권한을 불필요하게 요청하지 마세요. 앱은 저장소 관련 권한을 요청하지 않고도 MediaStore.Downloads 컬렉션을 포함하여 잘 정의된 미디어 컬렉션에 참여할 수 있습니다. 예를 들어 카메라 앱을 개발하고 있다면 미디어 저장소에 쓰고 있는 이미지를 앱이 소유하고 있으므로 저장소 관련 권한을 요청할 필요가 없습니다.

다른 앱에서 생성한 파일에 액세스하려면 다음 조건이 각각 충족되어야 합니다.

특히 앱에서 직접 생성하지 않은 MediaStore.Downloads 컬렉션 내의 파일에 액세스하려고 한다면 저장소 액세스 프레임워크를 사용해야 합니다. 이 프레임워크를 사용하는 방법에 관해 자세히 알아보려면 문서 및 기타 파일에 액세스하는 방법에 관한 가이드를 참조하세요.

범위 지정 저장소 사용 불가능

앱이 Android 9 이하를 실행하는 기기에서 사용되거나 저장소 호환성 기능을 사용하고 있다면 미디어 파일에 액세스하기 위해서는 READ_EXTERNAL_STORAGE 권한을 요청해야 합니다. 미디어 파일을 수정하려면 WRITE_EXTERNAL_STORAGE 권한도 요청해야 합니다.

미디어 위치 정보 액세스 권한

앱이 범위 지정 저장소를 사용할 때 앱이 사진에서 수정되지 않은 Exif 메타데이터를 검색할 수 있으려면 앱의 매니페스트에서 ACCESS_MEDIA_LOCATION 권한을 선언한 후 런타임 시 이 권한을 요청해야 합니다.

미디어 컬렉션 쿼리

5분 이상의 길이와 같은 특정 조건 집합을 충족하는 미디어를 찾으려면 다음 코드 스니펫과 유사한 SQL 선택 문을 사용합니다.

Kotlin

// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didn't create.

// Container for information about each video.
data class Video(val uri: Uri,
    val name: String,
    val duration: Int,
    val size: Int
)
val videoList = mutableListOf<Video>()

val projection = arrayOf(
    MediaStore.Video.Media._ID,
    MediaStore.Video.Media.DISPLAY_NAME,
    MediaStore.Video.Media.DURATION,
    MediaStore.Video.Media.SIZE
)

// Show only videos that are at least 5 minutes in duration.
val selection = "${MediaStore.Video.Media.DURATION} >= ?"
val selectionArgs = arrayOf(
    TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES).toString()
)

// Display videos in alphabetical order based on their display name.
val sortOrder = "${MediaStore.Video.Media.DISPLAY_NAME} ASC"

val query = ContentResolver.query(
    MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder
)
query?.use { cursor ->
    // Cache column indices.
    val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
    val nameColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
    val durationColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)
    val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)

    while (cursor.moveToNext()) {
        // Get values of columns for a given video.
        val id = cursor.getLong(idColumn)
        val name = cursor.getString(nameColumn)
        val duration = cursor.getInt(durationColumn)
        val size = cursor.getInt(sizeColumn)

        val contentUri: Uri = ContentUris.withAppendedId(
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
            id
        )

        // Stores column values and the contentUri in a local object
        // that represents the media file.
        videoList += Video(contentUri, name, duration, size)
    }
}

자바

// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didn't create.

// Container for information about each video.
class Video {
    private final Uri uri;
    private final String name;
    private final int duration;
    private final int size;

    public Video(Uri uri, String name, int duration, int size) {
        this.uri = uri;
        this.name = name;
        this.duration = duration;
        this.size = size;
    }
}
List<Video> videoList = new ArrayList<Video>();

String[] projection = new String[] {
    MediaStore.Video.Media._ID,
    MediaStore.Video.Media.DISPLAY_NAME,
    MediaStore.Video.Media.DURATION,
    MediaStore.Video.Media.SIZE
};
String selection = MediaStore.Video.Media.DURATION +
        " >= ?";
String[] selectionArgs = new String[] {
    String.valueOf(TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES));
};
String sortOrder = MediaStore.Video.Media.DISPLAY_NAME + " ASC";

try (Cursor cursor = getApplicationContext().getContentResolver().query(
    MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder
)) {
    // Cache column indices.
    int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
    int nameColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME);
    int durationColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION);
    int sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE);

    while (cursor.moveToNext()) {
        // Get values of columns for a given video.
        long id = cursor.getLong(idColumn);
        String name = cursor.getString(nameColumn);
        int duration = cursor.getInt(durationColumn);
        int size = cursor.getInt(sizeColumn);

        Uri contentUri = ContentUris.withAppendedId(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);

        // Stores column values and the contentUri in a local object
        // that represents the media file.
        videoList.add(new Video(contentUri, name, duration, size));
    }
}

앱에서 이러한 쿼리를 실행할 때 다음 사항에 유의하세요.

  • 작업자 스레드에서 query() 메서드를 호출합니다.
  • 쿼리 결과에서 행을 처리할 때마다 getColumnIndexOrThrow()를 호출할 필요가 없도록 열 색인을 캐시합니다.
  • 코드 스니펫에서와 같이 콘텐츠 URI에 ID를 추가합니다.
  • Android 10 이상을 실행하는 기기에는 MediaStore API에 정의된 열 이름이 필요합니다. 앱 내의 종속 라이브러리에서 API에 정의되지 않은 열 이름(예: "MimeType")이 필요하다면 CursorWrapper를 사용하여 앱 프로세스의 열 이름을 동적으로 변환합니다.

파일 미리보기 이미지 로드

앱에서 여러 미디어 파일을 표시하고 사용자에게 이러한 파일 중 하나를 선택하도록 요청하는 경우 파일을 직접 로드하는 대신 파일의 미리보기 버전 또는 미리보기 이미지를 로드하는 것이 더 효율적입니다.

특정 미디어 파일의 미리보기 이미지를 로드하려면 다음 코드 스니펫에서와 같이 loadThumbnail()을 사용하여 로드하려는 미리보기 이미지의 크기를 전달합니다.

Kotlin

// Load thumbnail of a specific media item.
val thumbnail: Bitmap =
        applicationContext.contentResolver.loadThumbnail(
        content-uri, Size(640, 480), null)

자바

// Load thumbnail of a specific media item.
Bitmap thumbnail =
        getApplicationContext().getContentResolver().loadThumbnail(
        content-uri, new Size(640, 480), null);

미디어 파일 열기

미디어 파일을 여는 데 사용하는 구체적인 로직은 미디어 콘텐츠가 파일 설명자로 가장 잘 표현되는지 또는 파일 스트림으로 가장 잘 표현되는지에 따라 다릅니다.

파일 설명자

파일 설명자를 사용하여 미디어 파일을 열려면 다음 코드 스니펫과 비슷한 로직을 사용합니다.

Kotlin

// Open a specific media item using ParcelFileDescriptor.
val resolver = applicationContext.contentResolver

// "rw" for read-and-write;
// "rwt" for truncating or overwriting existing file contents.
val readOnlyMode = "r"
resolver.openFileDescriptor(content-uri, readOnlyMode).use { pfd ->
    // Perform operations on "pfd".
}

자바

// Open a specific media item using ParcelFileDescriptor.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();

// "rw" for read-and-write;
// "rwt" for truncating or overwriting existing file contents.
String readOnlyMode = "r";
try (ParcelFileDescriptor pfd =
        resolver.openFileDescriptor(content-uri, readOnlyMode)) {
    // Perform operations on "pfd".
} catch (IOException e) {
    e.printStackTrace();
}

파일 스트림

파일 스트림을 사용하여 미디어 파일을 열려면 다음 코드 스니펫과 비슷한 로직을 사용합니다.

Kotlin

// Open a specific media item using InputStream.
val resolver = applicationContext.contentResolver
resolver.openInputStream(content-uri).use { stream ->
    // Perform operations on "stream".
}

자바

// Open a specific media item using InputStream.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();
try (InputStream stream = resolver.openInputStream(content-uri)) {
    // Perform operations on "stream".
}

미디어 콘텐츠에 액세스할 때의 고려사항

미디어 콘텐츠에 액세스할 때 다음 섹션에서 설명하는 고려사항에 유의하세요.

저장소 볼륨

Android 10 이상을 타겟팅하는 앱은 시스템에서 각 외부 저장소 볼륨에 할당한 고유 이름에 액세스할 수 있습니다. 이 이름 지정 시스템을 통해 효율적으로 콘텐츠를 구성하고 콘텐츠 색인을 생성하며 새 미디어 파일의 저장 위치를 관리할 수 있습니다.

기본 공유 저장소 볼륨은 항상 VOLUME_EXTERNAL_PRIMARY라고 합니다. 다른 볼륨은 MediaStore.getExternalVolumeNames()를 호출하여 탐색할 수 있습니다.

Kotlin

val volumeNames: Set<String> = MediaStore.getExternalVolumeNames(context)
val firstVolumeName = volumeNames.iterator().next()

자바

Set<String> volumeNames = MediaStore.getExternalVolumeNames(context);
String firstVolumeName = volumeNames.iterator().next();

사진의 위치 정보

일부 사진의 경우 사용자가 사진이 촬영된 장소를 볼 수 있는 위치 정보가 Exif 메타데이터에 들어 있습니다. 그러나 이 위치 정보는 민감한 정보이므로 앱이 범위 지정 저장소를 사용하는 경우 Android 10은 기본적으로 이 정보를 앱에서 숨깁니다.

앱이 사진 위치 정보에 액세스해야 하는 경우 다음 단계를 완료하세요.

  1. 앱의 매니페스트에서 ACCESS_MEDIA_LOCATION 권한을 요청합니다.
  2. 다음 코드 스니펫에서와 같이 MediaStore 객체에서 setRequireOriginal()을 호출하여 사진의 정확한 바이트를 가져오고 사진의 URI를 전달합니다.

    Kotlin

    val photoUri: Uri = Uri.withAppendedPath(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            cursor.getString(idColumnIndex)
    )
    
    // Get location data using the Exifinterface library.
    // Exception occurs if ACCESS_MEDIA_LOCATION permission isn't granted.
    photoUri = MediaStore.setRequireOriginal(photoUri)
    contentResolver.openInputStream(photoUri)?.use { stream ->
        ExifInterface(stream).run {
            // If lat/long is null, fall back to the coordinates (0, 0).
            val latLong = latLong ?: doubleArrayOf(0.0, 0.0)
        }
    }
    

    자바

    Uri photoUri = Uri.withAppendedPath(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            cursor.getString(idColumnIndex));
    
    final double[] latLong;
    
    // Get location data using the Exifinterface library.
    // Exception occurs if ACCESS_MEDIA_LOCATION permission isn't granted.
    photoUri = MediaStore.setRequireOriginal(photoUri);
    InputStream stream = getContentResolver().openInputStream(photoUri);
    if (stream != null) {
        ExifInterface exifInterface = new ExifInterface(stream);
        double[] returnedLatLong = exifInterface.getLatLong();
    
        // If lat/long is null, fall back to the coordinates (0, 0).
        latLong = returnedLatLong != null ? returnedLatLong : new double[2];
    
        // Don't reuse the stream associated with
        // the instance of "ExifInterface".
        stream.close();
    } else {
        // Failed to load the stream, so return the coordinates (0, 0).
        latLong = new double[2];
    }
    

미디어 공유

일부 앱에서는 사용자가 다른 사용자와 미디어 파일을 공유하도록 허용합니다. 예를 들어 소셜 미디어 앱을 사용하면 사용자가 친구와 사진 및 동영상을 공유할 수 있습니다.

미디어 파일을 공유하려면 콘텐츠 제공업체 만들기 가이드에서 권장하는 대로 content:// URI를 사용합니다.

원시 파일 경로를 사용한 콘텐츠 액세스

저장소 관련 권한이 없으면 File API를 사용하여 앱별 디렉터리의 파일과 앱에 속하는 미디어 파일에 액세스할 수 있습니다.

앱에서 File API를 사용하여 파일에 액세스하려고 하지만 필요한 권한이 없다면 FileNotFoundException이 발생합니다.

Android 10을 실행하는 기기에서 공유 저장소의 다른 파일에 액세스하려면 앱의 매니페스트 파일에서 requestLegacyExternalStoragetrue로 설정하여 범위 지정 저장소를 선택 해제하는 것이 좋습니다.

네이티브 코드에서 콘텐츠 액세스

다른 앱이 내 앱과 공유한 파일이나 사용자 미디어 컬렉션의 미디어 파일과 같이 네이티브 코드로 된 특정 미디어 파일을 앱에서 사용해야 하는 상황이 발생할 수 있습니다.

앱에서 fopen()과 같은 네이티브 파일 메서드를 사용하여 미디어 파일을 읽으려면 다음을 실행해야 합니다.

  1. 앱의 매니페스트 파일에서 requestLegacyExternalStoragetrue로 설정합니다.
  2. READ_EXTERNAL_STORAGE 권한을 요청합니다.

이러한 미디어 파일에 작성해야 한다면 자바 기반 코드나 Koltin 기반 코드의 파일 관련 파일 설명자를 네이티브 코드에 전달합니다. 다음 코드 스니펫은 미디어 객체의 파일 설명자를 앱의 네이티브 코드로 전달하는 방법을 보여줍니다.

Kotlin

val contentUri: Uri = ContentUris.withAppendedId(
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
        cursor.getLong(BaseColumns._ID))
val fileOpenMode = "r"
val parcelFd = resolver.openFileDescriptor(contentUri, fileOpenMode)
val fd = parcelFd?.detachFd()
// Pass the integer value "fd" into your native code. Remember to call
// close(2) on the file descriptor when you're done using it.

자바

Uri contentUri = ContentUris.withAppendedId(
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
        cursor.getLong(Integer.parseInt(BaseColumns._ID)));
String fileOpenMode = "r";
ParcelFileDescriptor parcelFd =
        resolver.openFileDescriptor(contentUri, fileOpenMode);
if (parcelFd != null) {
    int fd = parcelFd.detachFd();
    // Pass the integer value "fd" into your native code. Remember to call
    // close(2) on the file descriptor when you're done using it.
}

네이티브 코드로 된 파일에 액세스하는 방법을 자세히 알아보려면 Android Dev Summit '18에서 15:20부터 시작하는 Files for Miles 대화를 참조하세요.

앱의 미디어 파일 저작자 표시

Android 10 이상을 타겟팅하는 앱에서 범위 지정 저장소를 사용 설정하면 시스템은 앱에 각 미디어 파일 저작자 표시를 지정하여 앱이 저장소 권한을 요청하지 않았을 때 액세스할 수 있는 파일을 결정합니다. 각 파일의 저작자 표시는 하나의 앱에만 지정될 수 있습니다. 따라서 앱이 사진, 동영상 또는 오디오 파일 미디어 컬렉션에 저장되는 미디어 파일을 생성하면 앱은 그 파일에 액세스할 수 있습니다.

그러나 사용자가 앱을 제거했다가 재설치하는 경우 앱이 처음에 생성한 파일에 액세스하려면 READ_EXTERNAL_STORAGE를 요청해야 합니다. 이 권한 요청이 필요한 이유는 시스템에서 새로 설치된 앱이 아닌 이전에 설치된 앱 버전에 파일의 저작자 표시가 지정된 것으로 간주하기 때문입니다.

항목 추가

기존 컬렉션에 미디어 항목을 추가하려면 다음과 유사한 코드를 호출하세요.

Kotlin

// Add a specific media item.
val resolver = applicationContext.contentResolver

// Find all audio files on the primary external storage device.
// On API <= 28, use VOLUME_EXTERNAL instead.
val audioCollection = MediaStore.Audio.Media.getContentUri(
        MediaStore.VOLUME_EXTERNAL_PRIMARY)

// Publish a new song.
val newSongDetails = ContentValues().apply {
    put(MediaStore.Audio.Media.DISPLAY_NAME, "My Song.mp3")
}

// Keeps a handle to the new song's URI in case we need to modify it
// later.
val myFavoriteSongUri = resolver
        .insert(audioCollection, newSongDetails)

자바

// Add a specific media item.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();

// Find all audio files on the primary external storage device.
// On API <= 28, use VOLUME_EXTERNAL instead.
Uri audioCollection = MediaStore.Audio.Media.getContentUri(
        MediaStore.VOLUME_EXTERNAL_PRIMARY);

// Publish a new song.
ContentValues newSongDetails = new ContentValues();
newSongDetails.put(MediaStore.Audio.Media.DISPLAY_NAME,
        "My Song.mp3");

// Keeps a handle to the new song's URI in case we need to modify it
// later.
Uri myFavoriteSongUri = resolver
        .insert(audioCollection, newSongDetails);

미디어 파일의 대기 중 상태 전환

앱이 미디어 파일에 쓰기 작업을 하는 것과 같이 시간이 많이 소요될 수 있는 작업을 실행한다면 작업을 처리하는 동안 파일에 독점적으로 액세스하는 것이 유용합니다. Android 10 이상을 실행하는 기기에서는 앱이 IS_PENDING 플래그 값을 1로 설정하여 이 독점 액세스 권한을 얻을 수 있습니다. IS_PENDING 값을 다시 0으로 변경할 때까지 이 앱에서만 파일을 볼 수 있습니다.

다음 코드 스니펫은 이전 코드 스니펫을 기반으로 합니다. 이 스니펫은 MediaStore.Audio 컬렉션에 해당하는 디렉터리에 긴 노래를 저장할 때 IS_PENDING 플래그를 사용하는 방법을 보여줍니다.

Kotlin

// Add a media item that other apps shouldn't see until the item is
// fully written to the media store.
val resolver = applicationContext.contentResolver

// Find all audio files on the primary external storage device.
// On API <= 28, use VOLUME_EXTERNAL instead.
val audioCollection = MediaStore.Audio.Media
        .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)

val songDetails = ContentValues().apply {
    put(MediaStore.Audio.Media.DISPLAY_NAME, "My Workout Playlist.mp3")
    put(MediaStore.Audio.Media.IS_PENDING, 1)
}

val songContentUri = resolver.insert(audioCollection, songDetails)

resolver.openFileDescriptor(songContentUri, "w", null).use { pfd ->
    // Write data into the pending audio file.
}

// Now that we're finished, release the "pending" status, and allow other apps
// to play the audio track.
songDetails.clear()
songDetails.put(MediaStore.Audio.Media.IS_PENDING, 0)
resolver.update(songContentUri, songDetails, null, null)

자바

// Add a media item that other apps shouldn't see until the item is
// fully written to the media store.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();

// Find all audio files on the primary external storage device.
// On API <= 28, use VOLUME_EXTERNAL instead.
Uri audioCollection = MediaStore.Audio.Media.getContentUri(
        MediaStore.VOLUME_EXTERNAL_PRIMARY);

ContentValues songDetails = new ContentValues();
newSongDetails.put(MediaStore.Audio.Media.DISPLAY_NAME,
        "My Workout Playlist.mp3");
newSongDetails.put(MediaStore.Audio.Media.IS_PENDING, 1);

Uri songContentUri = resolver
        .insert(audioCollection, songDetails);

try (ParcelableFileDescriptor pfd =
        resolver.openFileDescriptor(longSongContentUri, "w", null)) {
    // Write data into the pending audio file.
}

// Now that we're finished, release the "pending" status, and allow other apps
// to play the audio track.
songDetails.clear();
songDetails.put(MediaStore.Audio.Media.IS_PENDING, 0);
resolver.update(longSongContentUri, songDetails, null, null);

파일 위치와 관련된 힌트 제공

앱이 Android 10을 실행하는 기기에 미디어를 저장할 때 미디어는 기본적으로 유형에 따라 정리됩니다. 예를 들어 새 이미지 파일은 기본적으로 Environment.DIRECTORY_PICTURES 디렉터리(MediaStore.Images 컬렉션에 상응)에 배치됩니다.

앱이 Pictures/MyVacationPictures라는 사진 앨범과 같이 파일을 저장해야 하는 구체적인 위치를 알고 있다면 MediaColumns.RELATIVE_PATH를 설정하여 새로 작성된 파일을 저장할 위치와 관련된 힌트를 시스템에 제공할 수 있습니다.

항목 업데이트

앱이 소유한 미디어 파일을 업데이트하려면 다음과 유사한 코드를 실행합니다.

Kotlin

// Updates an existing media item.
val mediaId = // MediaStore.Audio.Media._ID of item to update.
val resolver = applicationContext.contentResolver

// When performing a single item update, prefer using the ID
val selection = "${MediaStore.Audio.Media._ID} = ?"

// By using selection + args we protect against improper escaping of // values.
val selectionArgs = arrayOf(mediaId.toString())

// Update an existing song.
val updatedSongDetails = ContentValues().apply {
    put(MediaStore.Audio.Media.DISPLAY_NAME, "My Favorite Song.mp3")
}

// Use the individual song's URI to represent the collection that's
// updated.
val numSongsUpdated = resolver.update(
        myFavoriteSongUri,
        updatedSongDetails,
        selection,
        selectionArgs)

자바

// Updates an existing media item.
long mediaId = // MediaStore.Audio.Media._ID of item to update.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();

// When performing a single item update, prefer using the ID
String selection = MediaStore.Audio.Media._ID + " = ?";

// By using selection + args we protect against improper escaping of
// values. Here, "song" is an in-memory object that caches the song's
// information.
String[] selectionArgs = new String[] { getId().toString() };

// Update an existing song.
ContentValues updatedSongDetails = new ContentValues();
updatedSongDetails.put(MediaStore.Audio.Media.DISPLAY_NAME,
        "My Favorite Song.mp3");

// Use the individual song's URI to represent the collection that's
// updated.
int numSongsUpdated = resolver.update(
        myFavoriteSongUri,
        updatedSongDetails,
        selection,
        selectionArgs);

범위 지정 저장소를 사용할 수 없거나 사용 설정하지 않았다면 이전 코드 스니펫에 나와 있는 프로세스는 앱이 소유하지 않은 파일에도 적용됩니다.

다른 앱의 미디어 파일 업데이트

앱이 범위 지정 저장소를 사용하면 일반적으로 다른 앱이 미디어 저장소에 제공한 미디어 파일을 업데이트할 수 없습니다.

하지만 플랫폼에서 발생한 RecoverableSecurityException을 포착하여 여전히 파일 수정을 위한 사용자 동의를 받을 수 있습니다. 그런 다음, 아래의 코드 스니펫에서와 같이 특정 항목에 관한 쓰기 권한을 앱에 부여하도록 사용자에게 요청할 수 있습니다.

Kotlin

// Apply a grayscale filter to the image at the given content URI.
try {
    contentResolver.openFileDescriptor(image-content-uri, "w")?.use {
        setGrayscaleFilter(it)
    }
} catch (securityException: SecurityException) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        val recoverableSecurityException = securityException as?
            RecoverableSecurityException ?:
            throw RuntimeException(securityException.message, securityException)

        val intentSender =
            recoverableSecurityException.userAction.actionIntent.intentSender
        intentSender?.let {
            startIntentSenderForResult(intentSender, image-request-code,
                    null, 0, 0, 0, null)
        }
    } else {
        throw RuntimeException(securityException.message, securityException)
    }
}

자바

try {
    ParcelFileDescriptor imageFd = getContentResolver()
            .openFileDescriptor(image-content-uri, "w");
    setGrayscaleFilter(imageFd);
} catch (SecurityException securityException) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        RecoverableSecurityException recoverableSecurityException;
        if (securityException instanceof RecoverableSecurityException) {
            recoverableSecurityException =
                    (RecoverableSecurityException)securityException;
        } else {
            throw new RuntimeException(
                    securityException.getMessage(), securityException);
        }
        IntentSender intentSender =recoverableSecurityException.getUserAction()
                .getActionIntent().getIntentSender();
        startIntentSenderForResult(intentSender, image-request-code,
                null, 0, 0, 0, null);
    } else {
        throw new RuntimeException(
                securityException.getMessage(), securityException);
    }
}

앱이 생성하지 않은 미디어 파일을 수정해야 할 때마다 이 프로세스를 완료합니다.

앱에 범위 지정 저장소가 적용되지 않는 다른 사용 사례가 있다면 기능 요청을 제출하고 플랫폼에서 제공하는 앱 호환성 기능을 사용하세요.

항목 삭제

미디어 저장소에서 앱에 더 이상 필요하지 않은 항목을 삭제하려면 다음 코드 스니펫과 비슷한 로직을 사용합니다.

Kotlin

// Remove a specific media item.
val resolver = applicationContext.contentResolver

// URI of the image to remove.
val imageUri = "..."

// WHERE clause.
val selection = "..."
val selectionArgs = "..."

// Perform the actual removal.
val numImagesRemoved = resolver.delete(
        imageUri,
        selection,
        selectionArgs)

자바

// Remove a specific media item.
ContentResolver resolver = getApplicationContext()
        getContentResolver();

// URI of the image to remove.
Uri imageUri = "...";

// WHERE clause.
String selection = "...";
String[] selectionArgs = "...";

// Perform the actual removal.
int numImagesRemoved = resolver.delete(
        imageUri,
        selection,
        selectionArgs);

범위 지정 저장소를 사용할 수 없거나 사용 설정하지 않았다면 이전 코드 스니펫을 사용하여 다른 앱이 소유한 파일을 삭제할 수 있습니다. 그러나 범위 지정 저장소를 사용 설정했다면 미디어 항목 업데이트 섹션에 설명된 대로 앱이 삭제하려는 각 파일의 RecoverableSecurityException을 포착해야 합니다.

앱에 범위 지정 저장소가 적용되지 않는 다른 사용 사례가 있다면 기능 요청을 제출하고 플랫폼에서 제공하는 앱 호환성 기능을 사용하세요.

미디어 저장소의 대안이 필요한 사용 사례

앱이 주로 다음 역할 중 하나를 실행하면 MediaStore API의 대안을 고려하세요.

미디어 파일 그룹 관리

미디어 제작 앱은 일반적으로 디렉터리 계층 구조를 사용하여 파일 그룹을 관리합니다. 앱에서 이 기능을 제공하려면 문서 및 기타 파일을 저장하고 액세스하는 방법에 관한 가이드에 설명된 대로 ACTION_OPEN_DOCUMENT_TREE 인텐트 작업을 사용합니다.

다른 유형의 파일 작업

앱이 EPUB 또는 PDF 파일 확장자를 사용하는 파일과 같이 미디어 콘텐츠를 독점적으로 포함하지 않는 문서 및 파일 관련 작업을 한다면 문서 및 기타 파일을 저장하고 액세스하는 방법에 관한 가이드에 설명된 대로 ACTION_OPEN_DOCUMENT 인텐트 작업을 사용합니다.

호환 앱의 파일 공유

메시지 앱 및 프로필 앱과 같은 일련의 호환 앱을 제공하는 경우 content:// URI를 사용하여 파일 공유를 설정하세요. 이 워크플로는 보안 권장사항으로 권장됩니다.

추가 리소스

미디어를 저장하고 액세스하는 방법에 관한 자세한 내용은 다음 리소스를 참조하세요.

샘플

동영상