כדי לספק חוויית משתמש עשירה יותר, אפליקציות רבות מאפשרות למשתמשים להוסיף מדיה שזמינה בנפח אחסון חיצוני ולגשת אליה. המסגרת מספקת אינדקס אופטימלי לאוספי מדיה, שנקרא מאגר מדיה, שמאפשר למשתמשים לאחזר ולעדכן את קובצי המדיה האלה. גם אחרי שהאפליקציה תוסר, הקבצים האלה יישארו במכשיר של המשתמש.
בורר התמונות
במקום להשתמש במאגר המדיה, אפשר להשתמש בכלי לבחירת תמונות ב-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. עם זאת, מומלץ להשתמש בממשקי ה-API שלMediaStoreכדי לפתוח קבצים שהאפליקציה לא יצרה. - אם נפח האחסון הייעודי לאפליקציות לא זמין או לא בשימוש, באוסף מוצגים כל סוגי קובצי המדיה.
בקשת ההרשאות הנדרשות
לפני שמבצעים פעולות בקובצי מדיה, צריך לוודא שהאפליקציה הצהירה על ההרשאות שנדרשות לה כדי לגשת לקבצים האלה. עם זאת, חשוב לא להצהיר על הרשאות שהאפליקציה לא צריכה או לא משתמשת בהן.
הרשאות אחסון
הצורך של האפליקציה בהרשאות גישה לאחסון תלוי בשאלה אם היא ניגשת רק לקובצי המדיה שלה או לקבצים שנוצרו על ידי אפליקציות אחרות.
גישה לקובצי המדיה שלכם
במכשירים עם 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)); } }
כשמבצעים שאילתה כזו באפליקציה, חשוב לזכור את הדברים הבאים:
- מבצעים קריאה ל-
query()ב-Thread עובד. - שומרים במטמון את האינדקסים של העמודות כדי שלא תצטרכו להפעיל את
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 - ספריות Native, כמו
fopen()
אם אין לכם הרשאות שקשורות לאחסון, אתם יכולים לגשת לקבצים בספרייה הספציפית לאפליקציה וגם לקובצי מדיה שמשויכים לאפליקציה באמצעות ה-API של File.
אם האפליקציה מנסה לגשת לקובץ באמצעות ה-API של File ואין לה את ההרשאות הנדרשות, מתרחשת שגיאה מסוג FileNotFoundException.
כדי לגשת לקבצים אחרים בנפח האחסון המשותף במכשיר עם Android 10 (רמת API 29), מומלץ לבטל באופן זמני את ההסכמה לשימוש בנפח אחסון ייעודי לאפליקציות על ידי הגדרת requestLegacyExternalStorage לערך true בקובץ המניפסט של האפליקציה. כדי לגשת לקובצי מדיה באמצעות שיטות של קבצים מקוריים ב-Android מגרסה 10, צריך גם לבקש את ההרשאה READ_EXTERNAL_STORAGE.
שיקולים לגבי גישה לתוכן מדיה
כשניגשים לתוכן מדיה, חשוב לזכור את השיקולים שמפורטים בקטעים הבאים.
נתונים בקובץ שמור
אם האפליקציה שומרת במטמון מזהי URI או נתונים ממאגר המדיה, צריך לבדוק מדי פעם אם יש עדכונים במאגר המדיה. הבדיקה הזו מאפשרת לסנכרן בין הנתונים שנשמרו במטמון בצד האפליקציה לבין נתוני הספק בצד המערכת.
ביצועים
כשמבצעים קריאות רציפות של קובצי מדיה באמצעות נתיבי קבצים ישירים, הביצועים דומים לאלה של ה-API של MediaStore.
עם זאת, כשמבצעים קריאות וכתיבות אקראיות של קובצי מדיה באמצעות נתיבי קבצים ישירים, התהליך יכול להיות איטי עד פי שניים. במצבים כאלה, מומלץ להשתמש ב-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();
המיקום שבו צולמה המדיה
חלק מהתמונות והסרטונים מכילים נתוני מיקום במטא-נתונים שלהם, שמראים את המקום שבו התמונה צולמה או הסרטון הוקלט.
הדרך שבה ניגשים לנתוני המיקום האלה באפליקציה תלויה בשאלה אם צריך לגשת לנתוני המיקום של תמונה או של סרטון.
תצלומים
אם האפליקציה משתמשת בנפח אחסון ייעודי לאפליקציות, המערכת מסתירה את נתוני המיקום כברירת מחדל. כדי לגשת למידע הזה:
- מבקשים את ההרשאה
ACCESS_MEDIA_LOCATIONבמניפסט של האפליקציה. באובייקט
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://, כמו שמומלץ במדריך ליצירת ספק לניהול גישה.
שיוך של קובצי מדיה לאפליקציות
כשנפח אחסון ייעודי לאפליקציות מופעל באפליקציה שמטרגטת את 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);
אם נפח האחסון הייעודי לאפליקציות לא זמין או לא מופעל, התהליך שמוצג בקטע הקוד שלמעלה פועל גם בקבצים שלא בבעלות האפליקציה.
עדכון בקוד Native
אם אתם צריכים לכתוב קובצי מדיה באמצעות ספריות Native, העבירו את תיאור הקובץ שמשויך לקובץ מהקוד שמבוסס על Java או על Kotlin אל קוד ה-Native.
בקטע הקוד הבא אפשר לראות איך להעביר תיאור קובץ של אובייקט מדיה לקוד ה-Native של האפליקציה:
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 ואילך, אתם יכולים לבקש מהמשתמש לבחור קבוצה של קובצי מדיה, ואז לעדכן את קובצי המדיה האלה בפעולה אחת. השיטות האלה מספקות עקביות טובה יותר בין מכשירים, ומקלות על המשתמשים לנהל את אוספי המדיה שלהם.
ה-methods שמספקות את הפונקציונליות הזו של "חבילת עדכונים" כוללות את:
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) ומעלה, אתם יכולים לבקש מהמשתמשים לתת לאפליקציה את ההרשאה המיוחדת ניהול מדיה. ההרשאה הזו מאפשרת לאפליקציה לבצע את הפעולות הבאות בלי לבקש מהמשתמש אישור לכל פעולה על קובץ:
- משנים את הקבצים באמצעות
createWriteRequest(). - להעביר קבצים לאשפה ולהוציא אותם ממנה באמצעות
createTrashRequest(). - מוחקים קבצים באמצעות
createDeleteRequest().
כדי לעשות את זה:
מצהירים על ההרשאה
MANAGE_MEDIAועל ההרשאהREAD_EXTERNAL_STORAGEבקובץ המניפסט של האפליקציה.כדי להתקשר אל
createWriteRequest()בלי להציג תיבת דו-שיח לאישור, צריך להצהיר גם על ההרשאהACCESS_MEDIA_LOCATION.באפליקציה, מציגים למשתמש ממשק משתמש שמסביר למה כדאי לו להעניק לאפליקציה גישה לניהול מדיה.
מפעילים את פעולת ה-intent
ACTION_REQUEST_MANAGE_MEDIA. הפעולה הזו מעבירה את המשתמשים למסך אפליקציות לניהול מדיה בהגדרות המערכת. מכאן, המשתמשים יכולים להעניק לאפליקציה גישה מיוחדת.
תרחישים לדוגמה שבהם נדרשת חלופה למאגר המדיה
אם האפליקציה שלכם מבצעת בעיקר את אחד מהתפקידים הבאים, כדאי לשקול חלופה לממשקי ה-API של MediaStore.
עבודה עם סוגים אחרים של קבצים
אם האפליקציה שלכם פועלת עם מסמכים וקבצים שלא מכילים רק תוכן מדיה, כמו קבצים עם סיומת EPUB או PDF, אתם צריכים להשתמש בפעולת ה-Intent ACTION_OPEN_DOCUMENT, כפי שמתואר במדריך בנושא אחסון מסמכים וקבצים אחרים וגישה אליהם.
שיתוף קבצים באפליקציות נלוות
אם אתם מספקים חבילה של אפליקציות נלוות, כמו אפליקציית הודעות ואפליקציית פרופיל, הגדירו שיתוף קבצים באמצעות מזהי URI של content://. אנחנו ממליצים על תהליך העבודה הזה גם כשיטה מומלצת לאבטחה.
מקורות מידע נוספים
למידע נוסף על אחסון מדיה וגישה אליה, עיינו במקורות המידע הבאים.
דוגמאות
- MediaStore, זמין ב-GitHub