לגשת לקובצי מדיה מנפח אחסון משותף

כדי לספק חוויית משתמש משופרת, אפליקציות רבות מאפשרות למשתמשים להוסיף תוכן ולגשת למדיה שזמינה בנפח אחסון חיצוני. ה-framework מספק אינדקס אופטימלי לאוספים של מדיה, שנקרא חנות המדיה, שמאפשר למשתמשים לאחזר ולעדכן את קובצי המדיה האלה בקלות רבה יותר. גם אחרי הסרת האפליקציה, הקבצים האלה יישארו במכשיר של המשתמש.

בורר התמונות

כחלופה לשימוש בחנות המדיה, הכלי לבחירת תמונות ב-Android מספק למשתמשים דרך מובנית ובטוחה לבחור קובצי מדיה בלי להעניק לאפליקציה גישה לכל ספריית המדיה שלהם. התכונה הזו זמינה רק במכשירים נתמכים. מידע נוסף זמין במדריך בנושא הכלי לבחירת תמונות.

חנות מדיה

כדי לקיים אינטראקציה עם הפשטה של מאגר המדיה, משתמשים באובייקט 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. למידע נוסף על השימוש ב-framework הזה, ראו גישה למסמכים ולקבצים אחרים מאחסון משותף.

הרשאת מיקום של מדיה

אם האפליקציה שלכם מטרגטת את Android 10 (רמת API 29) ואילך, ואתם צריכים לאחזר מטא-נתונים של EXIF ללא צנזור מתמונות, עליכם להצהיר על ההרשאה ACCESS_MEDIA_LOCATION במניפסט של האפליקציה, ולאחר מכן לבקש את ההרשאה הזו בסביבת זמן הריצה.

איך בודקים אם יש עדכונים לחנות המדיה

כדי לגשת לקובצי מדיה בצורה אמינה יותר, במיוחד אם האפליקציה שומרת במטמון מזהי 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));
    }
}

כשמבצעים שאילתה כזו באפליקציה, חשוב לזכור את הנקודות הבאות:

  • קוראים ל-method‏ query() בשרשור של עובד.
  • שומרים במטמון את אינדקסי העמודות כדי שלא תצטרכו להפעיל את getColumnIndexOrThrow() בכל פעם שאתם מעבדים שורה מתוצאת השאילתה.
  • יש לצרף את המזהה ל-URI של התוכן כפי שמוצג בדוגמה הזו.
  • במכשירים עם Android מגרסה 10 ואילך, צריך להגדיר שמות של עמודות ב-API של MediaStore. אם ספרייה תלויה באפליקציה מצפה לשם עמודה שלא מוגדר ב-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 אחרים מלבד ה-API MediaStore כדי לגשת לקובצי מדיה מאחסון שיתופי. במקום זאת, אפשר לגשת ישירות לקובצי מדיה באמצעות אחד מ-API הבאים:

  • ה-API של File
  • ספריות מקוריות, כמו fopen()

אם אין לכם הרשאות שקשורות לאחסון, תוכלו לגשת לקבצים בתיקייה הספציפית לאפליקציה וגם לקבצי מדיה שמשויכים לאפליקציה באמצעות ה-API של File.

אם האפליקציה מנסה לגשת לקובץ באמצעות ה-API File ואין לה את ההרשאות הנדרשות, מתרחשת הודעת השגיאה FileNotFoundException.

כדי לגשת לקבצים אחרים באחסון השיתופי במכשיר עם Android 10 (רמת API 29), מומלץ לבטל באופן זמני את ההסכמה לאחסון מוגבל על ידי הגדרת requestLegacyExternalStorage לערך true בקובץ המניפסט של האפליקציה. כדי לגשת לקובצי מדיה באמצעות שיטות קבצים מקוריות ב-Android 10, צריך לבקש גם את ההרשאה READ_EXTERNAL_STORAGE.

שיקולים לגבי גישה לתוכן מדיה

כשאתם ניגשים לתוכן מדיה, חשוב לזכור את השיקולים שמפורטים בקטעים הבאים.

נתונים בקובץ שמור

אם האפליקציה שומרת במטמון מזהי URI או נתונים מחנות המדיה, כדאי לבדוק מדי פעם אם יש עדכונים בחנות המדיה. הבדיקה הזו מאפשרת לנתונים שנשמרו במטמון בצד האפליקציה להישאר מסונכרנים עם הנתונים של הספק בצד המערכת.

ביצועים

כשמבצעים קריאות רצופות של קובצי מדיה באמצעות נתיבי קבצים ישירים, הביצועים דומים לאלה של ממשק ה-API MediaStore.

עם זאת, כשמבצעים קריאות וכתיבה אקראיות של קובצי מדיה באמצעות נתיבים ישירים של קבצים, התהליך יכול להיות איטי פי שניים. במקרים כאלה, מומלץ להשתמש ב-API MediaStore במקום זאת.

העמודה DATA

כשאתם ניגשים לקובץ מדיה קיים, אתם יכולים להשתמש בערך של העמודה DATA בלוגיקה שלכם. הסיבה לכך היא שלערך הזה יש נתיב קובץ תקין. עם זאת, אל תניחו שהקובץ תמיד זמין. חשוב להיות מוכנים לטפל בשגיאות קלט/פלט מבוססות-קבצים שמתרחשות.

לעומת זאת, כדי ליצור או לעדכן קובץ מדיה לא משתמשים בערך של העמודה 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 במניפסט של האפליקציה.
  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);
}

שיתוף

באפליקציות מסוימות המשתמשים יכולים לשתף קבצי מדיה ביניהם. לדוגמה, אפליקציות של רשתות חברתיות מאפשרות למשתמשים לשתף תמונות וסרטונים עם חברים.

כדי לשתף קובצי מדיה, משתמשים ב-URI content://, בהתאם להמלצה במדריך ליצירת ספק תוכן.

שיוך (Attribution) של קובצי מדיה לאפליקציה

כשאחסון מוגבל מופעל באפליקציה שמטרגטת את Android מגרסה 10 ואילך, המערכת משייכת אפליקציה לכל קובץ מדיה. כך המערכת קובעת לאילו קבצים לאפליקציה תהיה גישה אם היא לא ביקשה הרשאות אחסון. אפשר לשייך כל קובץ רק לאפליקציה אחת. לכן, אם האפליקציה שלכם יוצרת קובץ מדיה שמאוחסן באוסף התמונות, הסרטונים או קובצי האודיו, לאפליקציה תהיה גישה לקובץ.

עם זאת, אם המשתמש מסיר את האפליקציה ומתקין אותה מחדש, תצטרכו לבקש מ-READ_EXTERNAL_STORAGE גישה לקבצים שהאפליקציה יצרה במקור. הבקשה להרשאה נדרשת כי המערכת מתייחסת לקובץ כקשור לגרסה הקודמת של האפליקציה, ולא לגרסה החדשה שהותקנה.

הוספת פריט

כדי להוסיף פריט מדיה לאוסף קיים, משתמשים בקוד דומה לקוד הבא. קטע הקוד הזה מאפשר גישה לנפח 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 ואילך, תוכלו לאפשר למשתמשים להעניק לאפליקציה הרשאת כתיבה לקבוצה של קובצי מדיה. משתמשים ב-method‏ createWriteRequest(), כפי שמתואר בקטע ניהול קבוצות של קובצי מדיה.

אם לאפליקציה שלכם יש תרחיש אחר לדוגמה שלא נכלל בנפח האחסון בהיקף, אתם יכולים לשלוח בקשה להוספת תכונה ולהפסיק באופן זמני את השימוש בנפח האחסון בהיקף.

הסרת פריט

כדי להסיר פריט שהאפליקציה לא צריכה יותר בחנות המדיה, צריך להשתמש בלוגיקה שדומה לזו שמוצגת בקטע הקוד הבא:

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 ואילך, תוכלו לאפשר למשתמשים לבחור קבוצה של קובצי מדיה להסרה. משתמשים ב-method‏ createTrashRequest() או ב-method‏ createDeleteRequest(), כפי שמתואר בקטע ניהול קבוצות של קובצי מדיה.

אם יש לאפליקציה שלכם תרחיש לדוגמה נוסף שלא מכוסה בנפח האחסון הייעודי לאפליקציות, שלחו בקשה להוספת תכונה וביטלו את ההשתתפות בנפח האחסון הייעודי לאפליקציות באופן זמני.

זיהוי עדכונים בקבצי מדיה

יכול להיות שהאפליקציה שלכם תצטרך לזהות נפחי אחסון שמכילים קובצי מדיה שאפליקציות הוסיפו או שינו, בהשוואה לנקודת זמן קודמת. כדי לזהות את השינויים האלה בצורה מהימנה ככל האפשר, מעבירים את נפח האחסון הרצוי אל getGeneration(). כל עוד הגרסה של חנות המדיה לא משתנה, ערך ההחזרה של השיטה הזו עולה באופן מונוטוני עם הזמן.

באופן ספציפי, getGeneration() חזק יותר מהתאריכים בעמודות מדיה, כמו DATE_ADDED ו-DATE_MODIFIED. הסיבה לכך היא שערכים של עמודות מדיה יכולים להשתנות כשאפליקציה קוראת ל-setLastModified() או כשהמשתמש משנה את שעון המערכת.

ניהול קבוצות של קובצי מדיה

ב-Android 11 ואילך, אפשר לבקש מהמשתמש לבחור קבוצה של קובצי מדיה, ואז לעדכן את קובצי המדיה האלה בפעולה אחת. השיטות האלה מספקות עקביות טובה יותר במכשירים שונים, ומאפשרות למשתמשים לנהל בקלות את אוספי המדיה שלהם.

השיטות שמספקות את הפונקציונליות של 'עדכון באצווה' כוללות את האפשרויות הבאות:

createWriteRequest()
מבקשים מהמשתמש להעניק לאפליקציה גישת כתיבה לקבוצה שצוינה של קובצי מדיה.
createFavoriteRequest()
מבקשים מהמשתמש לסמן את קובצי המדיה שצוינו כחלק מהמדיה 'המועדפת' שלו במכשיר. כל אפליקציה שיש לה גישת קריאה לקובץ הזה יכולה לראות שהמשתמש סימן את הקובץ כ'מועדף'.
createTrashRequest()

מבקשים מהמשתמש להעביר את קובצי המדיה שצוינו לאשפה של המכשיר. התמונות והסרטונים שבאשפה נמחקים לתמיד אחרי תקופת זמן שהוגדרה על ידי המערכת.

createDeleteRequest()

מבקשים מהמשתמש למחוק באופן סופי את קובצי המדיה שצוינו באופן מיידי, בלי להעביר אותם קודם לפח האשפה.

אחרי שמפעילים אחת מהשיטות האלה, המערכת יוצרת אובייקט PendingIntent. אחרי שהאפליקציה מפעילה את הכוונה הזו, המשתמשים רואים תיבת דו-שיח שבה הם מתבקשים לאשר לאפליקציה לעדכן או למחוק את קובצי המדיה שצוינו.

לדוגמה, כך המבנה של קריאה ל-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 בקובץ המניפסט של האפליקציה.

    כדי לקרוא ל-createWriteRequest() בלי להציג תיבת דו-שיח לאישור, צריך להצהיר גם על ההרשאה ACCESS_MEDIA_LOCATION.

  2. באפליקציה, מציגים למשתמש ממשק משתמש עם הסבר על הסיבות שבגללן כדאי לתת לאפליקציה גישה לניהול מדיה.

  3. קוראים לפעולת ה-Intent‏ ACTION_REQUEST_MANAGE_MEDIA. הפעולה הזו תעביר את המשתמשים למסך האפליקציות לניהול מדיה בהגדרות המערכת. מכאן המשתמשים יכולים להעניק לאפליקציה המיוחדת הרשאת גישה.

תרחישים לדוגמה שמחייבים חלופה לחנות המדיה

אם האפליקציה שלכם מבצעת בעיקר את אחד מהתפקידים הבאים, כדאי לכם לשקול חלופה לממשקי ה-API של MediaStore.

עבודה עם סוגים אחרים של קבצים

אם האפליקציה שלכם עובדת עם מסמכים וקבצים שלא מכילים אך ורק תוכן מדיה, כמו קבצים עם הסיומת EPUB או PDF, צריך להשתמש בפעולה של הכוונה ACTION_OPEN_DOCUMENT, כפי שמתואר במדריך בנושא אחסון של מסמכים וקבצים אחרים וגישה אליהם.

שיתוף קבצים באפליקציות נלוות

במקרים שבהם אתם מספקים חבילת אפליקציות נלוות, כמו אפליקציית הודעות ואפליקציית פרופיל, מגדירים שיתוף קבצים באמצעות מזהי URI מסוג content://. אנחנו ממליצים על תהליך העבודה הזה גם כשיטה מומלצת לאבטחה.

מקורות מידע נוספים

למידע נוסף על אחסון מדיה ועל גישה אליה, אפשר לעיין במקורות המידע הבאים:

דוגמיות

סרטונים