เข้าถึงไฟล์สื่อจากพื้นที่เก็บข้อมูลที่ใช้ร่วมกัน

แอปจำนวนมากอนุญาตให้ผู้ใช้มีส่วนร่วมและเข้าถึงสื่อที่อยู่ในวอลุ่มพื้นที่เก็บข้อมูลภายนอกเพื่อมอบประสบการณ์การใช้งานที่ดียิ่งขึ้น เฟรมเวิร์ก มีดัชนีที่เพิ่มประสิทธิภาพแล้วในคอลเล็กชันสื่อ ซึ่งเรียกว่าที่เก็บสื่อ ซึ่งช่วยให้ผู้ใช้เรียกข้อมูลและอัปเดตไฟล์สื่อเหล่านี้ได้ง่ายขึ้น แม้ว่าจะถอนการติดตั้งแอปแล้ว ไฟล์เหล่านี้จะยังคงอยู่ในอุปกรณ์ของผู้ใช้

เครื่องมือเลือกรูปภาพ

เครื่องมือเลือกรูปภาพของ Android เป็นทางเลือกแทนการใช้ Media Store ซึ่งเป็นวิธีที่ปลอดภัยและมีอยู่แล้วในตัวสำหรับให้ผู้ใช้เลือกไฟล์สื่อได้โดยไม่ต้อง ให้สิทธิ์แอปของคุณเข้าถึงคลังสื่อทั้งหมด ใช้ได้เฉพาะกับอุปกรณ์ที่รองรับเท่านั้น ดูข้อมูลเพิ่มเติมได้ในคู่มือเครื่องมือเลือกรูปภาพ

ร้านขายสื่อ

หากต้องการโต้ตอบกับ MediaStore Abstraction ให้ใช้ออบเจ็กต์ 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.
    }
}

Java

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/ รวมถึงไฟล์บันทึกเสียงที่อยู่ในไดเรกทอรี Recordings/ ด้วย ระบบจะเพิ่มไฟล์เหล่านี้ลงในตาราง MediaStore.Audio ไดเรกทอรี Recordings/ ไม่พร้อมใช้งานใน Android 11 (API ระดับ 30) และต่ำกว่า
  • ไฟล์ที่ดาวน์โหลดซึ่งจัดเก็บไว้ในไดเรกทอรี Download/ ในอุปกรณ์ที่ใช้ Android 10 (API ระดับ 29) ขึ้นไป ระบบจะจัดเก็บไฟล์เหล่านี้ไว้ในตาราง MediaStore.Downloads ตารางนี้ไม่พร้อมใช้งานใน Android 9 (API ระดับ 28) และต่ำกว่า

นอกจากนี้ ร้านค้าสื่อยังมีคอลเล็กชันชื่อ MediaStore.Files เนื้อหาของไฟล์นี้จะขึ้นอยู่กับว่าแอปของคุณใช้ที่เก็บข้อมูลที่จำกัดขอบเขต การเข้าถึงหรือไม่ ซึ่งจะใช้ได้กับแอปที่กำหนดเป้าหมายเป็น Android 10 ขึ้นไป

  • หากเปิดใช้พื้นที่เก็บข้อมูลที่จำกัดขอบเขต คอลเล็กชันจะแสดงเฉพาะรูปภาพ วิดีโอ และไฟล์เสียงที่แอปของคุณสร้างขึ้น นักพัฒนาแอปส่วนใหญ่ไม่จำเป็นต้องใช้ MediaStore.Files เพื่อดูไฟล์สื่อจากแอปอื่นๆ แต่หากคุณมีข้อกำหนดเฉพาะ ในการดำเนินการดังกล่าว คุณสามารถประกาศสิทธิ์ READ_EXTERNAL_STORAGE ได้ อย่างไรก็ตาม เราขอแนะนำให้คุณใช้ MediaStore API เพื่อเปิดไฟล์ที่แอปไม่ได้สร้าง
  • หากที่เก็บข้อมูลที่จำกัดขอบเขตไม่พร้อมใช้งานหรือไม่ได้ใช้ คอลเล็กชันจะแสดงไฟล์สื่อทุกประเภท

ขอสิทธิ์ที่จำเป็น

ก่อนดำเนินการกับไฟล์สื่อ โปรดตรวจสอบว่าแอปของคุณได้ประกาศ สิทธิ์ที่จำเป็นต่อการเข้าถึงไฟล์เหล่านี้แล้ว อย่างไรก็ตาม โปรดระมัดระวังไม่ให้ ประกาศสิทธิ์ที่แอปไม่จำเป็นต้องใช้หรือไม่ได้ใช้

สิทธิ์ในพื้นที่เก็บข้อมูล

แอปของคุณต้องมีสิทธิ์เข้าถึงพื้นที่เก็บข้อมูลหรือไม่นั้นขึ้นอยู่กับว่าแอปเข้าถึง เฉพาะไฟล์สื่อของตัวเองหรือไฟล์ที่สร้างโดยแอปอื่นๆ

เข้าถึงไฟล์สื่อของคุณเอง

ในอุปกรณ์ที่ใช้ Android 10 ขึ้นไป คุณไม่จำเป็นต้องมีสิทธิ์ที่เกี่ยวข้องกับพื้นที่เก็บข้อมูลเพื่อเข้าถึงและแก้ไขไฟล์สื่อที่แอปของคุณเป็นเจ้าของ รวมถึงไฟล์ในMediaStore.Downloads คอลเล็กชัน เช่น หากคุณกำลังพัฒนาแอปกล้อง คุณไม่จำเป็นต้องขอสิทธิ์ที่เกี่ยวข้องกับพื้นที่เก็บข้อมูลเพื่อเข้าถึงรูปภาพที่ถ่าย เนื่องจากแอปของคุณเป็นเจ้าของรูปภาพที่คุณเขียนลงในร้านค้าสื่อ

เข้าถึงไฟล์สื่อของแอปอื่นๆ

หากต้องการเข้าถึงไฟล์สื่อที่แอปอื่นๆ สร้างขึ้น คุณต้องประกาศสิทธิ์ที่เกี่ยวข้องกับการจัดเก็บที่เหมาะสม และไฟล์ต้องอยู่ในคอลเล็กชันสื่อต่อไปนี้

ตราบใดที่ดูไฟล์ได้จากคำค้นหา MediaStore.Images MediaStore.Video หรือ MediaStore.Audio ก็จะดูได้โดยใช้คำค้นหา MediaStore.Files ด้วย

ข้อมูลโค้ดต่อไปนี้แสดงวิธีประกาศสิทธิ์เข้าถึงพื้นที่เก็บข้อมูลที่เหมาะสม

<!-- Required only if your app needs to access images or photos
     that other apps created. -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

<!-- Required only if your app needs to access videos
     that other apps created. -->
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

<!-- Required only if your app needs to access audio files
     that other apps created. -->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

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

สิทธิ์เพิ่มเติมที่จำเป็นสำหรับแอปที่ทำงานบนอุปกรณ์รุ่นเดิม

หากแอปของคุณใช้ในอุปกรณ์ที่ใช้ Android 9 หรือต่ำกว่า หรือหากแอปเลือกไม่ใช้พื้นที่เก็บข้อมูลที่จำกัดขอบเขตชั่วคราว คุณต้องขอสิทธิ์ READ_EXTERNAL_STORAGE เพื่อเข้าถึงไฟล์สื่อ หากต้องการแก้ไขไฟล์สื่อ คุณต้องขอสิทธิ์ WRITE_EXTERNAL_STORAGE ด้วย

ต้องใช้ Storage Access Framework เพื่อเข้าถึงรายการที่ดาวน์โหลดของแอปอื่นๆ

หากแอปต้องการเข้าถึงไฟล์ภายในMediaStore.Downloadsคอลเล็กชัน ที่แอปไม่ได้สร้าง คุณต้องใช้ Storage Access Framework ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีใช้เฟรมเวิร์กนี้ได้ที่เข้าถึงเอกสารและไฟล์อื่นๆ จากพื้นที่เก็บข้อมูลที่แชร์

สิทธิ์เข้าถึงตำแหน่งของสื่อ

หากแอปกำหนดเป้าหมายเป็น Android 10 (API ระดับ 29) ขึ้นไปและต้อง ดึงข้อมูลเมตา EXIF ที่ไม่ได้แก้ไขออกจากรูปภาพ คุณต้องประกาศสิทธิ์ ACCESS_MEDIA_LOCATION ในไฟล์ Manifest ของแอป จากนั้นขอสิทธิ์นี้ในรันไทม์

ตรวจสอบการอัปเดตร้านค้าสื่อ

หากต้องการเข้าถึงไฟล์สื่อได้อย่างน่าเชื่อถือมากขึ้น โดยเฉพาะอย่างยิ่งหากแอปแคช URI หรือ ข้อมูลจากคลังสื่อ ให้ตรวจสอบว่าเวอร์ชันคลังสื่อมีการเปลี่ยนแปลงหรือไม่ เมื่อเทียบกับตอนที่คุณซิงค์ข้อมูลสื่อครั้งล่าสุด หากต้องการตรวจสอบการอัปเดต ให้โทรหา getVersion() เวอร์ชันที่แสดงคือสตริงที่ไม่ซ้ำกันซึ่งจะเปลี่ยนแปลงเมื่อใดก็ตามที่ร้านค้าสื่อ มีการเปลี่ยนแปลงอย่างมาก หากเวอร์ชันที่ส่งคืนแตกต่างจากเวอร์ชันที่ซิงค์ล่าสุด ให้สแกนและซิงค์แคชสื่อของแอปอีกครั้ง

ดำเนินการตรวจสอบนี้เมื่อเริ่มต้นกระบวนการของแอป คุณไม่จำเป็นต้องตรวจสอบ เวอร์ชันทุกครั้งที่ค้นหาสื่อในร้านค้า

อย่าคาดเดารายละเอียดการติดตั้งใช้งานเกี่ยวกับหมายเลขเวอร์ชัน

ค้นหาคอลเล็กชันสื่อ

หากต้องการค้นหาสื่อที่ตรงตามชุดเงื่อนไขที่เฉพาะเจาะจง เช่น ระยะเวลา 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 collection =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Video.Media.getContentUri(
            MediaStore.VOLUME_EXTERNAL
        )
    } else {
        MediaStore.Video.Media.EXTERNAL_CONTENT_URI
    }

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(
    collection,
    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)
    }
}

Java

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

Uri collection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
} else {
    collection = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
}

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(
    collection,
    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() ในเธรดของ Worker
  • แคชดัชนีคอลัมน์เพื่อให้คุณไม่ต้องเรียกใช้ getColumnIndexOrThrow() ทุกครั้งที่ประมวลผลแถวจากผลการค้นหา
  • ต่อท้ายรหัสกับ URI ของเนื้อหาตามที่แสดงในตัวอย่างนี้
  • อุปกรณ์ที่ใช้ 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)

Java

// 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".
}

Java

// 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".
}

Java

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

เส้นทางไฟล์โดยตรง

Android 11 (API ระดับ 30) ขึ้นไปช่วยให้คุณใช้ API อื่นนอกเหนือจาก MediaStore API เพื่อเข้าถึง ไฟล์สื่อจากพื้นที่เก็บข้อมูลที่แชร์ได้ เพื่อช่วยให้แอปทำงานร่วมกับคลังสื่อของบุคคลที่สามได้อย่างราบรื่นยิ่งขึ้น แต่คุณจะเข้าถึงไฟล์สื่อได้โดยตรง โดยใช้ API ใด API หนึ่งต่อไปนี้

  • File API
  • ไลบรารีที่มาพร้อมเครื่อง เช่น fopen()

หากไม่มีสิทธิ์ที่เกี่ยวข้องกับการจัดเก็บข้อมูล คุณจะเข้าถึงไฟล์ในไดเรกทอรีเฉพาะของแอป รวมถึงไฟล์สื่อที่เชื่อมโยงกับแอปของคุณได้โดยใช้ File API

หากแอปพยายามเข้าถึงไฟล์โดยใช้ File API และไม่มีสิทธิ์ที่จำเป็น จะเกิด FileNotFoundException ขึ้น

หากต้องการเข้าถึงไฟล์อื่นๆ ในพื้นที่เก็บข้อมูลที่ใช้ร่วมกันในอุปกรณ์ที่ใช้ Android 10 (API ระดับ 29) เราขอแนะนำให้เลือกไม่ใช้พื้นที่เก็บข้อมูลที่จำกัดขอบเขตชั่วคราวโดยตั้งค่า requestLegacyExternalStorage เป็น true ในไฟล์ Manifest ของแอป หากต้องการเข้าถึงไฟล์สื่อโดยใช้วิธีการของไฟล์ดั้งเดิมใน Android 10 คุณต้องขอสิทธิ์ READ_EXTERNAL_STORAGE ด้วย

ข้อควรพิจารณาเมื่อเข้าถึงเนื้อหาสื่อ

เมื่อเข้าถึงเนื้อหาสื่อ โปรดคำนึงถึงข้อควรพิจารณาที่กล่าวถึงในส่วนต่อไปนี้

ข้อมูลในแคช

หากแอปแคช URI หรือข้อมูลจาก MediaStore ให้ตรวจสอบ การอัปเดต MediaStore เป็นระยะ การตรวจสอบนี้ช่วยให้ข้อมูลที่แคชไว้ฝั่งแอปซิงค์กับข้อมูลผู้ให้บริการฝั่งระบบ

ประสิทธิภาพ

เมื่ออ่านไฟล์สื่อแบบต่อเนื่องโดยใช้เส้นทางไฟล์โดยตรง ประสิทธิภาพจะเทียบเท่ากับของ API MediaStore

อย่างไรก็ตาม เมื่อคุณอ่านและเขียนไฟล์สื่อแบบสุ่มโดยใช้เส้นทางไฟล์โดยตรง กระบวนการนี้อาจช้าลงถึง 2 เท่า ในกรณีเหล่านี้ เราขอแนะนำให้ใช้ MediaStore API แทน

คอลัมน์ DATA

เมื่อเข้าถึงไฟล์สื่อที่มีอยู่ คุณจะใช้ค่าของคอลัมน์ DATA ในตรรกะได้ เนื่องจากค่านี้มีเส้นทางไฟล์ที่ถูกต้อง อย่างไรก็ตาม อย่า คิดว่าไฟล์จะพร้อมใช้งานเสมอไป เตรียมพร้อมรับมือกับข้อผิดพลาดของ I/O ที่อิงตามไฟล์ที่เกิดขึ้น

ในทางกลับกัน หากต้องการสร้างหรืออัปเดตไฟล์สื่อ อย่าใช้ค่าของคอลัมน์ DATA ให้ใช้ค่าของคอลัมน์ DISPLAY_NAME และ RELATIVE_PATH แทน

ปริมาณการจัดเก็บ

แอปที่กำหนดเป้าหมายเป็น Android 10 ขึ้นไปจะเข้าถึงชื่อที่ไม่ซ้ำกัน ซึ่งระบบกำหนดให้กับวอลุ่มที่จัดเก็บข้อมูลภายนอกแต่ละรายการได้ ระบบการตั้งชื่อนี้ ช่วยให้คุณจัดระเบียบและจัดทำดัชนีเนื้อหาได้อย่างมีประสิทธิภาพ และช่วยให้คุณควบคุม ตำแหน่งที่จัดเก็บไฟล์สื่อใหม่ได้

คุณควรทราบข้อมูลต่อไปนี้

  • VOLUME_EXTERNAL วอลุ่มจะแสดงมุมมองของวอลุ่มพื้นที่เก็บข้อมูลที่แชร์ทั้งหมดในอุปกรณ์ คุณอ่าน เนื้อหาของวอลุ่มสังเคราะห์นี้ได้ แต่จะแก้ไขเนื้อหาไม่ได้
  • VOLUME_EXTERNAL_PRIMARY ปริมาณแสดงปริมาณพื้นที่เก็บข้อมูลที่แชร์หลักในอุปกรณ์ คุณสามารถ อ่านและแก้ไขเนื้อหาของไดรฟ์ข้อมูลนี้ได้

คุณค้นหาเล่มอื่นๆ ได้โดยเรียกใช้ MediaStore.getExternalVolumeNames()

Kotlin

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

Java

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

ตำแหน่งที่บันทึกสื่อ

ภาพถ่ายและวิดีโอบางรายการมีข้อมูลตำแหน่งในข้อมูลเมตา ซึ่งแสดงสถานที่ถ่ายภาพหรือสถานที่บันทึกวิดีโอ

วิธีเข้าถึงข้อมูลตำแหน่งนี้ในแอปจะขึ้นอยู่กับว่าคุณ ต้องเข้าถึงข้อมูลตำแหน่งสำหรับรูปภาพหรือวิดีโอ

ภาพถ่าย

หากแอปใช้พื้นที่เก็บข้อมูลที่จำกัดขอบเขต ระบบจะซ่อนข้อมูลตำแหน่งโดยค่าเริ่มต้น หากต้องการเข้าถึงข้อมูลนี้ ให้ทำตามขั้นตอนต่อไปนี้

  1. ขอสิทธิ์ ACCESS_MEDIA_LOCATION ในไฟล์ Manifest ของแอป
  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)
        }
    }

    Java

    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];
    }

วิดีโอ

หากต้องการเข้าถึงข้อมูลสถานที่ตั้งภายในข้อมูลเมตาของวิดีโอ ให้ใช้คลาส MediaMetadataRetriever ดังที่แสดงในข้อมูลโค้ดต่อไปนี้ แอปของคุณไม่จำเป็นต้องขอสิทธิ์เพิ่มเติมเพื่อใช้คลาสนี้

Kotlin

val retriever = MediaMetadataRetriever()
val context = applicationContext

// Find the videos that are stored on a device by querying the video collection.
val query = ContentResolver.query(
    collection,
    projection,
    selection,
    selectionArgs,
    sortOrder
)
query?.use { cursor ->
    val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
    while (cursor.moveToNext()) {
        val id = cursor.getLong(idColumn)
        val videoUri: Uri = ContentUris.withAppendedId(
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
            id
        )
        extractVideoLocationInfo(videoUri)
    }
}

private fun extractVideoLocationInfo(videoUri: Uri) {
    try {
        retriever.setDataSource(context, videoUri)
    } catch (e: RuntimeException) {
        Log.e(APP_TAG, "Cannot retrieve video file", e)
    }
    // Metadata uses a standardized format.
    val locationMetadata: String? =
            retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
}

Java

MediaMetadataRetriever retriever = new MediaMetadataRetriever();
Context context = getApplicationContext();

// Find the videos that are stored on a device by querying the video collection.
try (Cursor cursor = context.getContentResolver().query(
    collection,
    projection,
    selection,
    selectionArgs,
    sortOrder
)) {
    int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
    while (cursor.moveToNext()) {
        long id = cursor.getLong(idColumn);
        Uri videoUri = ContentUris.withAppendedId(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);
        extractVideoLocationInfo(videoUri);
    }
}

private void extractVideoLocationInfo(Uri videoUri) {
    try {
        retriever.setDataSource(context, videoUri);
    } catch (RuntimeException e) {
        Log.e(APP_TAG, "Cannot retrieve video file", e);
    }
    // Metadata uses a standardized format.
    String locationMetadata = retriever.extractMetadata(
            MediaMetadataRetriever.METADATA_KEY_LOCATION);
}

การแชร์

แอปบางแอปอนุญาตให้ผู้ใช้แชร์ไฟล์สื่อกับผู้ใช้รายอื่น เช่น แอปโซเชียลมีเดียช่วยให้ผู้ใช้แชร์รูปภาพและวิดีโอกับเพื่อนๆ ได้

หากต้องการแชร์ไฟล์สื่อ ให้ใช้ content:// URI ตามที่แนะนำในคำแนะนำ ในการสร้าง Content Provider

การระบุแหล่งที่มาของแอปสำหรับไฟล์สื่อ

เมื่อเปิดใช้ที่เก็บข้อมูลที่จำกัดขอบเขตสำหรับแอปที่กำหนดเป้าหมายเป็น Android 10 ขึ้นไป ระบบจะระบุแอปกับไฟล์สื่อแต่ละไฟล์ ซึ่งจะเป็นตัวกำหนดไฟล์ที่แอปเข้าถึงได้เมื่อแอปไม่ได้ขอสิทธิ์เข้าถึงที่เก็บข้อมูลใดๆ ไฟล์แต่ละไฟล์จะระบุแหล่งที่มาได้เพียงแอปเดียว ดังนั้นหากแอปสร้างไฟล์สื่อที่จัดเก็บไว้ในคอลเล็กชันสื่อของรูปภาพ วิดีโอ หรือไฟล์เสียง แอปของคุณจะมีสิทธิ์เข้าถึงไฟล์นั้น

อย่างไรก็ตาม หากผู้ใช้ถอนการติดตั้งและติดตั้งแอปของคุณอีกครั้ง คุณต้องขอ READ_EXTERNAL_STORAGE เพื่อเข้าถึงไฟล์ที่แอปของคุณสร้างขึ้นในตอนแรก คำขอสิทธิ์นี้ จำเป็นเนื่องจากระบบพิจารณาว่าไฟล์ดังกล่าวเป็นของแอปเวอร์ชันที่ ติดตั้งไว้ก่อนหน้านี้ ไม่ใช่เวอร์ชันที่ติดตั้งใหม่

เมื่อแอปที่กำหนดเป้าหมายเป็น SDK 36 ขึ้นไปในอุปกรณ์ที่ใช้ Android 16 ขึ้นไปแจ้งให้ขอสิทธิ์เข้าถึงรูปภาพและวิดีโอ ผู้ใช้ที่เลือกจำกัดการเข้าถึงสื่อที่เลือกจะเห็นรูปภาพทั้งหมดที่แอปเป็นเจ้าของซึ่งเลือกไว้ล่วงหน้าในเครื่องมือเลือกรูปภาพ ผู้ใช้สามารถยกเลิกการเลือกรายการที่เลือกไว้ล่วงหน้าเหล่านี้ ซึ่งจะเป็นการเพิกถอน สิทธิ์การเข้าถึงรูปภาพและวิดีโอเหล่านั้นของแอป

เพิ่มสินค้า

หากต้องการเพิ่มรายการสื่อลงในคอลเล็กชันที่มีอยู่ ให้ใช้โค้ดที่คล้ายกับโค้ดต่อไปนี้ ข้อมูลโค้ดนี้จะเข้าถึงVOLUME_EXTERNAL_PRIMARYระดับเสียง ในอุปกรณ์ที่ใช้ Android 10 ขึ้นไป เนื่องจากในอุปกรณ์เหล่านี้ คุณจะแก้ไขเนื้อหาของวอลุ่มได้ก็ต่อเมื่อเป็นวอลุ่มหลักเท่านั้น ดังที่อธิบายไว้ในส่วนวอลุ่มพื้นที่เก็บข้อมูล

Kotlin

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

// Find all audio files on the primary external storage device.
val audioCollection =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Audio.Media.getContentUri(
            MediaStore.VOLUME_EXTERNAL_PRIMARY
        )
    } else {
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
    }

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

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

Java

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

// Find all audio files on the primary external storage device.
Uri audioCollection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    audioCollection = MediaStore.Audio.Media
            .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
} else {
    audioCollection = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}

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

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

สลับสถานะรอดำเนินการสำหรับไฟล์สื่อ

หากแอปของคุณดำเนินการที่อาจใช้เวลานาน เช่น การเขียนไปยัง ไฟล์สื่อ การมีสิทธิ์เข้าถึงไฟล์แบบพิเศษจะเป็นประโยชน์เนื่องจากไฟล์กำลัง ประมวลผล ในอุปกรณ์ที่ใช้ Android 10 ขึ้นไป แอปของคุณจะ รับสิทธิ์เข้าถึงพิเศษนี้ได้โดยการตั้งค่าสถานะ IS_PENDING เป็น 1 มีเพียงแอปของคุณเท่านั้นที่จะดูไฟล์ได้จนกว่าแอปจะเปลี่ยนค่าของ IS_PENDING กลับเป็น 0

ข้อมูลโค้ดต่อไปนี้สร้างขึ้นจากข้อมูลโค้ดก่อนหน้า ข้อมูลโค้ดนี้แสดงวิธีใช้แฟล็ก IS_PENDING เมื่อจัดเก็บเพลงยาวใน ไดเรกทอรีที่สอดคล้องกับคอลเล็กชัน MediaStore.Audio

Kotlin

// Add a media item that other apps don'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.
val audioCollection =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Audio.Media.getContentUri(
            MediaStore.VOLUME_EXTERNAL_PRIMARY
        )
    } else {
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
    }

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)

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

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

Java

// Add a media item that other apps don'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.
Uri audioCollection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    audioCollection = MediaStore.Audio.Media
            .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
} else {
    audioCollection = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}

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

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

// "w" for write.
try (ParcelFileDescriptor pfd =
        resolver.openFileDescriptor(songContentUri, "w", null)) {
    // Write data into the pending audio file.
}

// Now that you're finished, release the "pending" status and let other apps
// play the audio track.
songDetails.clear();
songDetails.put(MediaStore.Audio.Media.IS_PENDING, 0);
resolver.update(songContentUri, 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 you 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)

Java

// 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 you 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);

หากที่เก็บข้อมูลที่จำกัดขอบเขตไม่พร้อมใช้งานหรือไม่ได้เปิดใช้ กระบวนการที่แสดงใน โค้ดตัวอย่างก่อนหน้าจะใช้ได้กับไฟล์ที่แอปของคุณไม่ได้เป็นเจ้าของด้วย

อัปเดตในโค้ดที่มาพร้อมเครื่อง

หากต้องการเขียนไฟล์สื่อโดยใช้ไลบรารีแบบเนทีฟ ให้ส่งตัวอธิบายไฟล์ที่เชื่อมโยงของไฟล์จากโค้ดที่ใช้ Java หรือ Kotlin ไปยังโค้ดแบบเนทีฟ

ข้อมูลโค้ดต่อไปนี้แสดงวิธีส่งตัวอธิบายไฟล์ของออบเจ็กต์สื่อ ไปยังโค้ดเนทีฟของแอป

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.

Java

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.
}

อัปเดตไฟล์สื่อของแอปอื่นๆ

หากแอปใช้พื้นที่เก็บข้อมูลที่จำกัดขอบเขต โดยปกติแล้ว แอปจะอัปเดตไฟล์สื่อที่แอปอื่นมีส่วนร่วมใน ที่เก็บสื่อไม่ได้

คุณขอความยินยอมจากผู้ใช้เพื่อแก้ไขไฟล์ได้โดยการตรวจหาRecoverableSecurityException ที่แพลตฟอร์มส่งมา จากนั้นคุณสามารถขอให้ผู้ใช้ให้สิทธิ์แอปของคุณ ในการเข้าถึงแบบเขียนไปยังรายการนั้นๆ ดังที่แสดงในข้อมูลโค้ดต่อไปนี้

Kotlin

// Apply a grayscale filter to the image at the given content URI.
try {
    // "w" for write.
    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)
    }
}

Java

try {
    // "w" for write.
    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);
    }
}

ทำกระบวนการนี้ทุกครั้งที่แอปต้องแก้ไขไฟล์สื่อที่ไม่ได้สร้างขึ้นเอง

หรือหากแอปทำงานบน Android 11 ขึ้นไป คุณสามารถ อนุญาตให้ผู้ใช้ให้สิทธิ์การเข้าถึงแบบเขียนแก่แอปของคุณในกลุ่มไฟล์สื่อได้ ใช้วิธี createWriteRequest() ตามที่อธิบายไว้ในส่วนเกี่ยวกับวิธีจัดการกลุ่มไฟล์สื่อ

หากแอปมี Use Case อื่นที่ไม่ได้ครอบคลุมโดยที่เก็บข้อมูลที่จำกัดขอบเขต ให้ยื่นคำขอฟีเจอร์และเลือกไม่ใช้ที่เก็บข้อมูลที่จำกัดขอบเขตชั่วคราว

นำรายการออก

หากต้องการนำรายการที่แอปไม่ต้องการในร้านค้าสื่อออก ให้ใช้ตรรกะ ที่คล้ายกับที่แสดงในข้อมูลโค้ดต่อไปนี้

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)

Java

// 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 สำหรับแต่ละไฟล์ที่แอปต้องการนำออก ตามที่อธิบายไว้ในส่วนเกี่ยวกับการอัปเดตรายการสื่อ

หากแอปทำงานบน Android 11 ขึ้นไป คุณสามารถอนุญาตให้ผู้ใช้ เลือกกลุ่มไฟล์สื่อที่จะนำออกได้ ใช้วิธี createTrashRequest() หรือวิธี createDeleteRequest() ตามที่อธิบายไว้ในส่วนเกี่ยวกับวิธีจัดการกลุ่มไฟล์สื่อ

หากแอปมี Use Case อื่นที่ไม่ได้ครอบคลุมโดยที่เก็บข้อมูลที่จำกัดขอบเขต ให้ยื่นคำขอฟีเจอร์และเลือกไม่ใช้ที่เก็บข้อมูลที่จำกัดขอบเขตชั่วคราว

ตรวจหาการอัปเดตไฟล์สื่อ

แอปอาจต้องระบุวอลุ่มพื้นที่เก็บข้อมูลที่มีไฟล์สื่อที่แอป เพิ่มหรือแก้ไข เมื่อเทียบกับช่วงเวลาก่อนหน้า หากต้องการตรวจหาการเปลี่ยนแปลงเหล่านี้ ได้อย่างน่าเชื่อถือที่สุด ให้ส่งระดับเสียงของพื้นที่เก็บข้อมูลที่ต้องการไปยัง getGeneration() ตราบใดที่เวอร์ชันของ MediaStore ไม่เปลี่ยนแปลง ค่าที่ส่งคืนของเมธอดนี้จะเพิ่มขึ้นอย่างต่อเนื่องเมื่อเวลาผ่านไป

โดยเฉพาะอย่างยิ่ง getGeneration() มีความเสถียรมากกว่าวันที่ในคอลัมน์สื่อ เช่น DATE_ADDED และ DATE_MODIFIED เนื่องจากค่าคอลัมน์สื่อเหล่านั้นอาจเปลี่ยนแปลงได้เมื่อแอปเรียกใช้ setLastModified() หรือเมื่อ ผู้ใช้เปลี่ยนนาฬิกาของระบบ

จัดการกลุ่มไฟล์สื่อ

ใน Android 11 ขึ้นไป คุณสามารถขอให้ผู้ใช้เลือกกลุ่ม ไฟล์สื่อ แล้วอัปเดตไฟล์สื่อเหล่านี้ในการดำเนินการครั้งเดียว ซึ่ง วิธีเหล่านี้ช่วยให้มีความสอดคล้องกันมากขึ้นในอุปกรณ์ต่างๆ และช่วยให้ผู้ใช้จัดการคอลเล็กชันสื่อได้ง่ายขึ้น

วิธีการที่ให้ฟังก์ชัน "อัปเดตแบบเป็นชุด" นี้ ได้แก่ รายการต่อไปนี้

createWriteRequest()
ขอให้ผู้ใช้ให้สิทธิ์การเขียนไฟล์สื่อในกลุ่มที่ระบุแก่แอปของคุณ
createFavoriteRequest()
ขอให้ผู้ใช้ทำเครื่องหมายไฟล์สื่อที่ระบุเป็นสื่อ "รายการโปรด" ในอุปกรณ์ แอปใดก็ตามที่มีสิทธิ์เข้าถึงแบบอ่านไฟล์นี้จะ เห็นว่าผู้ใช้ได้ทำเครื่องหมายไฟล์เป็น "รายการโปรด"
createTrashRequest()

ขอให้ผู้ใช้นำไฟล์สื่อที่ระบุไปไว้ในถังขยะของอุปกรณ์ ระบบจะลบรายการในถังขยะออกอย่างถาวรหลังจากระยะเวลาที่ระบบกำหนด

createDeleteRequest()

ขอให้ผู้ใช้ลบไฟล์สื่อที่ระบุอย่างถาวร ทันทีโดยไม่ต้องนำไปใส่ในถังขยะก่อน

หลังจากเรียกใช้เมธอดใดเมธอดหนึ่งเหล่านี้แล้ว ระบบจะสร้างออบเจ็กต์ PendingIntent หลังจากที่แอปของคุณเรียกใช้ Intent นี้ ผู้ใช้จะเห็นกล่องโต้ตอบที่ขอความยินยอมให้แอปของคุณอัปเดตหรือลบไฟล์สื่อที่ระบุ

เช่น วิธีจัดโครงสร้างการเรียกใช้ createWriteRequest() มีดังนี้

Kotlin

val urisToModify = /* A collection of content URIs to modify. */
val editPendingIntent = MediaStore.createWriteRequest(contentResolver,
        urisToModify)

// Launch a system prompt requesting user permission for the operation.
startIntentSenderForResult(editPendingIntent.intentSender, EDIT_REQUEST_CODE,
    null, 0, 0, 0)

Java

List<Uri> urisToModify = /* A collection of content URIs to modify. */
PendingIntent editPendingIntent = MediaStore.createWriteRequest(contentResolver,
                  urisToModify);

// Launch a system prompt requesting user permission for the operation.
startIntentSenderForResult(editPendingIntent.getIntentSender(),
    EDIT_REQUEST_CODE, null, 0, 0, 0);

ประเมินคำตอบของผู้ใช้ หากผู้ใช้ให้ความยินยอม ให้ดำเนินการต่อด้วย การดำเนินการกับสื่อ มิฉะนั้น ให้อธิบายให้ผู้ใช้ทราบว่าทำไมแอปของคุณจึงต้องการ สิทธิ์

Kotlin

override fun onActivityResult(requestCode: Int, resultCode: Int,
                 data: Intent?) {
    ...
    when (requestCode) {
        EDIT_REQUEST_CODE ->
            if (resultCode == Activity.RESULT_OK) {
                /* Edit request granted; proceed. */
            } else {
                /* Edit request not granted; explain to the user. */
            }
    }
}

Java

@Override
protected void onActivityResult(int requestCode, int resultCode,
                   @Nullable Intent data) {
    ...
    if (requestCode == EDIT_REQUEST_CODE) {
        if (resultCode == Activity.RESULT_OK) {
            /* Edit request granted; proceed. */
        } else {
            /* Edit request not granted; explain to the user. */
        }
    }
}

คุณใช้รูปแบบทั่วไปเดียวกันนี้กับ createFavoriteRequest() createTrashRequest() และ createDeleteRequest() ได้

สิทธิ์การจัดการสื่อ

ผู้ใช้อาจเชื่อถือแอปหนึ่งๆ ในการจัดการสื่อ เช่น การแก้ไขไฟล์สื่อบ่อยๆ หากแอปของคุณกำหนดเป้าหมายเป็น Android 11 ขึ้นไปและไม่ใช่แอปแกลเลอรีเริ่มต้นของอุปกรณ์ คุณต้องแสดงกล่องโต้ตอบการยืนยันต่อผู้ใช้ทุกครั้งที่แอปพยายาม แก้ไขหรือลบไฟล์

หากแอปกำหนดเป้าหมายเป็น Android 12 (API ระดับ 31) ขึ้นไป คุณสามารถขอให้ ผู้ใช้ให้สิทธิ์เข้าถึงแบบพิเศษสำหรับการจัดการสื่อแก่แอปได้ สิทธิ์นี้ช่วยให้แอปของคุณทำสิ่งต่อไปนี้ได้โดยไม่ต้องแจ้งให้ผู้ใช้ทราบสำหรับการดำเนินการกับไฟล์แต่ละรายการ

โดยทำตามขั้นตอนต่อไปนี้

  1. ประกาศสิทธิ์ MANAGE_MEDIA และสิทธิ์ READ_EXTERNAL_STORAGE ในไฟล์ Manifest ของแอป

    หากต้องการเรียกใช้ createWriteRequest() โดยไม่แสดงกล่องโต้ตอบการยืนยัน ให้ประกาศสิทธิ์ ACCESS_MEDIA_LOCATION ด้วย

  2. ในแอป ให้แสดง UI แก่ผู้ใช้เพื่ออธิบายเหตุผลที่ผู้ใช้อาจต้องการให้สิทธิ์ การเข้าถึงการจัดการสื่อแก่แอปของคุณ

  3. เรียกใช้การดำเนินการของ ACTION_REQUEST_MANAGE_MEDIA Intent ซึ่งจะนำผู้ใช้ไปยังหน้าจอแอปการจัดการสื่อในการตั้งค่าระบบ จากที่นี่ ผู้ใช้สามารถให้สิทธิ์เข้าถึงแอปพิเศษได้

Use Case ที่ต้องใช้ทางเลือกอื่นแทนร้านค้าสื่อ

หากแอปของคุณมีบทบาทอย่างใดอย่างหนึ่งต่อไปนี้เป็นหลัก ให้พิจารณาใช้MediaStore API อื่นแทน

การทำงานกับไฟล์ประเภทอื่นๆ

หากแอปของคุณทำงานกับเอกสารและไฟล์ที่ไม่ได้มีเฉพาะเนื้อหาสื่อ เช่น ไฟล์ที่ใช้นามสกุลไฟล์ EPUB หรือ PDF ให้ใช้ACTION_OPEN_DOCUMENTการดำเนินการของ Intent ตามที่อธิบายไว้ในคำแนะนำเกี่ยวกับการจัดเก็บและการเข้าถึงเอกสารและไฟล์อื่นๆ

การแชร์ไฟล์ในแอปที่ใช้ร่วมกัน

ในกรณีที่คุณมีชุดแอปคู่กัน เช่น แอปส่งข้อความและ แอปโปรไฟล์ ให้ตั้งค่าการแชร์ไฟล์ โดยใช้ URI ของ content:// นอกจากนี้ เราขอแนะนำให้ใช้วิธีนี้เป็นแนวทางปฏิบัติแนะนำด้านความปลอดภัยด้วย

แหล่งข้อมูลเพิ่มเติม

ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีจัดเก็บและเข้าถึงสื่อได้จากแหล่งข้อมูลต่อไปนี้

ตัวอย่าง

  • MediaStore พร้อมใช้งานบน GitHub

วิดีโอ