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

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

בורר התמונות

כחלופה לשימוש בחנות המדיה, הכלי לבחירת תמונות ב-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 הרשאה גם כן.

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

אם האפליקציה שלכם רוצה לגשת לקובץ באוסף MediaStore.Downloads שלא נוצר על ידה, עליכם להשתמש במסגרת Storage Access 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));
    }
}

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

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

ביצועים

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

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

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

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://, בהתאם להמלצה במדריך אל יצירת ספק תוכן.

ייחוס של קובצי מדיה באפליקציה

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

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

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

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

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

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

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

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

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

דוגמיות

סרטונים