2 * Copyright (C) 2007 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package android.media;
19 import android.content.ContentProviderClient;
20 import android.content.ContentResolver;
21 import android.content.ContentUris;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.content.SharedPreferences;
25 import android.database.Cursor;
26 import android.database.SQLException;
27 import android.drm.DrmManagerClient;
28 import android.graphics.BitmapFactory;
29 import android.mtp.MtpConstants;
30 import android.net.Uri;
31 import android.os.Build;
32 import android.os.Environment;
33 import android.os.RemoteException;
34 import android.os.SystemProperties;
35 import android.provider.MediaStore;
36 import android.provider.MediaStore.Audio;
37 import android.provider.MediaStore.Audio.Playlists;
38 import android.provider.MediaStore.Files;
39 import android.provider.MediaStore.Files.FileColumns;
40 import android.provider.MediaStore.Images;
41 import android.provider.MediaStore.Video;
42 import android.provider.Settings;
43 import android.provider.Settings.SettingNotFoundException;
44 import android.sax.Element;
45 import android.sax.ElementListener;
46 import android.sax.RootElement;
47 import android.system.ErrnoException;
48 import android.system.Os;
49 import android.text.TextUtils;
50 import android.util.Log;
51 import android.util.Xml;
53 import dalvik.system.CloseGuard;
55 import org.xml.sax.Attributes;
56 import org.xml.sax.ContentHandler;
57 import org.xml.sax.SAXException;
59 import java.io.BufferedReader;
61 import java.io.FileDescriptor;
62 import java.io.FileInputStream;
63 import java.io.IOException;
64 import java.io.InputStreamReader;
65 import java.text.SimpleDateFormat;
66 import java.text.ParseException;
67 import java.util.ArrayList;
68 import java.util.HashMap;
69 import java.util.HashSet;
70 import java.util.Iterator;
71 import java.util.Locale;
72 import java.util.TimeZone;
73 import java.util.concurrent.atomic.AtomicBoolean;
76 * Internal service helper that no-one should use directly.
78 * The way the scan currently works is:
79 * - The Java MediaScannerService creates a MediaScanner (this class), and calls
80 * MediaScanner.scanDirectories on it.
81 * - scanDirectories() calls the native processDirectory() for each of the specified directories.
82 * - the processDirectory() JNI method wraps the provided mediascanner client in a native
83 * 'MyMediaScannerClient' class, then calls processDirectory() on the native MediaScanner
84 * object (which got created when the Java MediaScanner was created).
85 * - native MediaScanner.processDirectory() calls
86 * doProcessDirectory(), which recurses over the folder, and calls
87 * native MyMediaScannerClient.scanFile() for every file whose extension matches.
88 * - native MyMediaScannerClient.scanFile() calls back on Java MediaScannerClient.scanFile,
89 * which calls doScanFile, which after some setup calls back down to native code, calling
90 * MediaScanner.processFile().
91 * - MediaScanner.processFile() calls one of several methods, depending on the type of the
92 * file: parseMP3, parseMP4, parseMidi, parseOgg or parseWMA.
93 * - each of these methods gets metadata key/value pairs from the file, and repeatedly
94 * calls native MyMediaScannerClient.handleStringTag, which calls back up to its Java
95 * counterparts in this file.
96 * - Java handleStringTag() gathers the key/value pairs that it's interested in.
97 * - once processFile returns and we're back in Java code in doScanFile(), it calls
98 * Java MyMediaScannerClient.endFile(), which takes all the data that's been
99 * gathered and inserts an entry in to the database.
102 * Java MediaScannerService calls
103 * Java MediaScanner scanDirectories, which calls
104 * Java MediaScanner processDirectory (native method), which calls
105 * native MediaScanner processDirectory, which calls
106 * native MyMediaScannerClient scanFile, which calls
107 * Java MyMediaScannerClient scanFile, which calls
108 * Java MediaScannerClient doScanFile, which calls
109 * Java MediaScanner processFile (native method), which calls
110 * native MediaScanner processFile, which calls
111 * native parseMP3, parseMP4, parseMidi, parseOgg or parseWMA, which calls
112 * native MyMediaScanner handleStringTag, which calls
113 * Java MyMediaScanner handleStringTag.
114 * Once MediaScanner processFile returns, an entry is inserted in to the database.
116 * The MediaScanner class is not thread-safe, so it should only be used in a single threaded manner.
120 public class MediaScanner implements AutoCloseable {
122 System.loadLibrary("media_jni");
126 private final static String TAG = "MediaScanner";
128 private static final String[] FILES_PRESCAN_PROJECTION = new String[] {
129 Files.FileColumns._ID, // 0
130 Files.FileColumns.DATA, // 1
131 Files.FileColumns.FORMAT, // 2
132 Files.FileColumns.DATE_MODIFIED, // 3
135 private static final String[] ID_PROJECTION = new String[] {
136 Files.FileColumns._ID,
139 private static final int FILES_PRESCAN_ID_COLUMN_INDEX = 0;
140 private static final int FILES_PRESCAN_PATH_COLUMN_INDEX = 1;
141 private static final int FILES_PRESCAN_FORMAT_COLUMN_INDEX = 2;
142 private static final int FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX = 3;
144 private static final String[] PLAYLIST_MEMBERS_PROJECTION = new String[] {
145 Audio.Playlists.Members.PLAYLIST_ID, // 0
148 private static final int ID_PLAYLISTS_COLUMN_INDEX = 0;
149 private static final int PATH_PLAYLISTS_COLUMN_INDEX = 1;
150 private static final int DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX = 2;
152 private static final String RINGTONES_DIR = "/ringtones/";
153 private static final String NOTIFICATIONS_DIR = "/notifications/";
154 private static final String ALARMS_DIR = "/alarms/";
155 private static final String MUSIC_DIR = "/music/";
156 private static final String PODCAST_DIR = "/podcasts/";
158 public static final String SCANNED_BUILD_PREFS_NAME = "MediaScanBuild";
159 public static final String LAST_INTERNAL_SCAN_FINGERPRINT = "lastScanFingerprint";
160 private static final String SYSTEM_SOUNDS_DIR = "/system/media/audio";
161 private static String sLastInternalScanFingerprint;
163 private static final String[] ID3_GENRES = {
245 // The following genres are Winamp extensions
292 // The following ones seem to be fairly widely supported as well
307 "Contemporary Christian",
315 // 148 and up don't seem to have been defined yet.
318 private long mNativeContext;
319 private final Context mContext;
320 private final String mPackageName;
321 private final String mVolumeName;
322 private final ContentProviderClient mMediaProvider;
323 private final Uri mAudioUri;
324 private final Uri mVideoUri;
325 private final Uri mImagesUri;
326 private final Uri mPlaylistsUri;
327 private final Uri mFilesUri;
328 private final Uri mFilesUriNoNotify;
329 private final boolean mProcessPlaylists;
330 private final boolean mProcessGenres;
331 private int mMtpObjectHandle;
333 private final AtomicBoolean mClosed = new AtomicBoolean();
334 private final CloseGuard mCloseGuard = CloseGuard.get();
336 /** whether to use bulk inserts or individual inserts for each item */
337 private static final boolean ENABLE_BULK_INSERTS = true;
339 // used when scanning the image database so we know whether we have to prune
340 // old thumbnail files
341 private int mOriginalCount;
342 /** Whether the scanner has set a default sound for the ringer ringtone. */
343 private boolean mDefaultRingtoneSet;
344 /** Whether the scanner has set a default sound for the notification ringtone. */
345 private boolean mDefaultNotificationSet;
346 /** Whether the scanner has set a default sound for the alarm ringtone. */
347 private boolean mDefaultAlarmSet;
348 /** The filename for the default sound for the ringer ringtone. */
349 private String mDefaultRingtoneFilename;
350 /** The filename for the default sound for the notification ringtone. */
351 private String mDefaultNotificationFilename;
352 /** The filename for the default sound for the alarm ringtone. */
353 private String mDefaultAlarmAlertFilename;
355 * The prefix for system properties that define the default sound for
356 * ringtones. Concatenate the name of the setting from Settings
357 * to get the full system property.
359 private static final String DEFAULT_RINGTONE_PROPERTY_PREFIX = "ro.config.";
361 private final BitmapFactory.Options mBitmapOptions = new BitmapFactory.Options();
363 private static class FileEntry {
368 boolean mLastModifiedChanged;
370 FileEntry(long rowId, String path, long lastModified, int format) {
373 mLastModified = lastModified;
375 mLastModifiedChanged = false;
379 public String toString() {
380 return mPath + " mRowId: " + mRowId;
384 private static class PlaylistEntry {
390 private final ArrayList<PlaylistEntry> mPlaylistEntries = new ArrayList<>();
391 private final ArrayList<FileEntry> mPlayLists = new ArrayList<>();
393 private MediaInserter mMediaInserter;
395 private DrmManagerClient mDrmManagerClient = null;
397 public MediaScanner(Context c, String volumeName) {
400 mPackageName = c.getPackageName();
401 mVolumeName = volumeName;
403 mBitmapOptions.inSampleSize = 1;
404 mBitmapOptions.inJustDecodeBounds = true;
406 setDefaultRingtoneFileNames();
408 mMediaProvider = mContext.getContentResolver()
409 .acquireContentProviderClient(MediaStore.AUTHORITY);
411 if (sLastInternalScanFingerprint == null) {
412 final SharedPreferences scanSettings =
413 mContext.getSharedPreferences(SCANNED_BUILD_PREFS_NAME, Context.MODE_PRIVATE);
414 sLastInternalScanFingerprint =
415 scanSettings.getString(LAST_INTERNAL_SCAN_FINGERPRINT, new String());
418 mAudioUri = Audio.Media.getContentUri(volumeName);
419 mVideoUri = Video.Media.getContentUri(volumeName);
420 mImagesUri = Images.Media.getContentUri(volumeName);
421 mFilesUri = Files.getContentUri(volumeName);
422 mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build();
424 if (!volumeName.equals("internal")) {
425 // we only support playlists on external media
426 mProcessPlaylists = true;
427 mProcessGenres = true;
428 mPlaylistsUri = Playlists.getContentUri(volumeName);
430 mProcessPlaylists = false;
431 mProcessGenres = false;
432 mPlaylistsUri = null;
435 final Locale locale = mContext.getResources().getConfiguration().locale;
436 if (locale != null) {
437 String language = locale.getLanguage();
438 String country = locale.getCountry();
439 if (language != null) {
440 if (country != null) {
441 setLocale(language + "_" + country);
448 mCloseGuard.open("close");
451 private void setDefaultRingtoneFileNames() {
452 mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
453 + Settings.System.RINGTONE);
454 mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
455 + Settings.System.NOTIFICATION_SOUND);
456 mDefaultAlarmAlertFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
457 + Settings.System.ALARM_ALERT);
460 private final MyMediaScannerClient mClient = new MyMediaScannerClient();
462 private boolean isDrmEnabled() {
463 String prop = SystemProperties.get("drm.service.enabled");
464 return prop != null && prop.equals("true");
467 private class MyMediaScannerClient implements MediaScannerClient {
469 private final SimpleDateFormat mDateFormatter;
471 private String mArtist;
472 private String mAlbumArtist; // use this if mArtist is missing
473 private String mAlbum;
474 private String mTitle;
475 private String mComposer;
476 private String mGenre;
477 private String mMimeType;
478 private int mFileType;
481 private int mDuration;
482 private String mPath;
484 private long mLastModified;
485 private long mFileSize;
486 private String mWriter;
487 private int mCompilation;
488 private boolean mIsDrm;
489 private boolean mNoMedia; // flag to suppress file from appearing in media tables
493 public MyMediaScannerClient() {
494 mDateFormatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
495 mDateFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
498 public FileEntry beginFile(String path, String mimeType, long lastModified,
499 long fileSize, boolean isDirectory, boolean noMedia) {
500 mMimeType = mimeType;
502 mFileSize = fileSize;
506 if (!noMedia && isNoMediaFile(path)) {
511 // try mimeType first, if it is specified
512 if (mimeType != null) {
513 mFileType = MediaFile.getFileTypeForMimeType(mimeType);
516 // if mimeType was not specified, compute file type based on file extension.
517 if (mFileType == 0) {
518 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
519 if (mediaFileType != null) {
520 mFileType = mediaFileType.fileType;
521 if (mMimeType == null) {
522 mMimeType = mediaFileType.mimeType;
527 if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) {
528 mFileType = getFileTypeFromDrm(path);
532 FileEntry entry = makeEntryFor(path);
533 // add some slack to avoid a rounding error
534 long delta = (entry != null) ? (lastModified - entry.mLastModified) : 0;
535 boolean wasModified = delta > 1 || delta < -1;
536 if (entry == null || wasModified) {
538 entry.mLastModified = lastModified;
540 entry = new FileEntry(0, path, lastModified,
541 (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
543 entry.mLastModifiedChanged = true;
546 if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
547 mPlayLists.add(entry);
548 // we don't process playlists in the main scan, so return null
552 // clear all the metadata
564 mLastModified = lastModified;
574 public void scanFile(String path, long lastModified, long fileSize,
575 boolean isDirectory, boolean noMedia) {
576 // This is the callback funtion from native codes.
577 // Log.v(TAG, "scanFile: "+path);
578 doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
581 public Uri doScanFile(String path, String mimeType, long lastModified,
582 long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
584 // long t1 = System.currentTimeMillis();
586 FileEntry entry = beginFile(path, mimeType, lastModified,
587 fileSize, isDirectory, noMedia);
593 // if this file was just inserted via mtp, set the rowid to zero
594 // (even though it already exists in the database), to trigger
595 // the correct code path for updating its entry
596 if (mMtpObjectHandle != 0) {
600 if (entry.mPath != null) {
601 if (((!mDefaultNotificationSet &&
602 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename))
603 || (!mDefaultRingtoneSet &&
604 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename))
605 || (!mDefaultAlarmSet &&
606 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)))) {
607 Log.w(TAG, "forcing rescan of " + entry.mPath +
608 "since ringtone setting didn't finish");
610 } else if (isSystemSoundWithMetadata(entry.mPath)
611 && !Build.FINGERPRINT.equals(sLastInternalScanFingerprint)) {
612 // file is located on the system partition where the date cannot be trusted:
613 // rescan if the build fingerprint has changed since the last scan.
614 Log.i(TAG, "forcing rescan of " + entry.mPath
615 + " since build fingerprint changed");
620 // rescan for metadata if file was modified since last scan
621 if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
623 result = endFile(entry, false, false, false, false, false);
625 String lowpath = path.toLowerCase(Locale.ROOT);
626 boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
627 boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
628 boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
629 boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
630 boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
631 (!ringtones && !notifications && !alarms && !podcasts);
633 boolean isaudio = MediaFile.isAudioFileType(mFileType);
634 boolean isvideo = MediaFile.isVideoFileType(mFileType);
635 boolean isimage = MediaFile.isImageFileType(mFileType);
637 if (isaudio || isvideo || isimage) {
638 path = Environment.maybeTranslateEmulatedPathToInternal(new File(path))
642 // we only extract metadata for audio and video files
643 if (isaudio || isvideo) {
644 processFile(path, mimeType, this);
648 processImageFile(path);
651 result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
654 } catch (RemoteException e) {
655 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
657 // long t2 = System.currentTimeMillis();
658 // Log.v(TAG, "scanFile: " + path + " took " + (t2-t1));
662 private long parseDate(String date) {
664 return mDateFormatter.parse(date).getTime();
665 } catch (ParseException e) {
670 private int parseSubstring(String s, int start, int defaultValue) {
671 int length = s.length();
672 if (start == length) return defaultValue;
674 char ch = s.charAt(start++);
675 // return defaultValue if we have no integer at all
676 if (ch < '0' || ch > '9') return defaultValue;
678 int result = ch - '0';
679 while (start < length) {
680 ch = s.charAt(start++);
681 if (ch < '0' || ch > '9') return result;
682 result = result * 10 + (ch - '0');
688 public void handleStringTag(String name, String value) {
689 if (name.equalsIgnoreCase("title") || name.startsWith("title;")) {
690 // Don't trim() here, to preserve the special \001 character
691 // used to force sorting. The media provider will trim() before
692 // inserting the title in to the database.
694 } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) {
695 mArtist = value.trim();
696 } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")
697 || name.equalsIgnoreCase("band") || name.startsWith("band;")) {
698 mAlbumArtist = value.trim();
699 } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) {
700 mAlbum = value.trim();
701 } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) {
702 mComposer = value.trim();
703 } else if (mProcessGenres &&
704 (name.equalsIgnoreCase("genre") || name.startsWith("genre;"))) {
705 mGenre = getGenreName(value);
706 } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) {
707 mYear = parseSubstring(value, 0, 0);
708 } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) {
709 // track number might be of the form "2/12"
710 // we just read the number before the slash
711 int num = parseSubstring(value, 0, 0);
712 mTrack = (mTrack / 1000) * 1000 + num;
713 } else if (name.equalsIgnoreCase("discnumber") ||
714 name.equals("set") || name.startsWith("set;")) {
715 // set number might be of the form "1/3"
716 // we just read the number before the slash
717 int num = parseSubstring(value, 0, 0);
718 mTrack = (num * 1000) + (mTrack % 1000);
719 } else if (name.equalsIgnoreCase("duration")) {
720 mDuration = parseSubstring(value, 0, 0);
721 } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {
722 mWriter = value.trim();
723 } else if (name.equalsIgnoreCase("compilation")) {
724 mCompilation = parseSubstring(value, 0, 0);
725 } else if (name.equalsIgnoreCase("isdrm")) {
726 mIsDrm = (parseSubstring(value, 0, 0) == 1);
727 } else if (name.equalsIgnoreCase("date")) {
728 mDate = parseDate(value);
729 } else if (name.equalsIgnoreCase("width")) {
730 mWidth = parseSubstring(value, 0, 0);
731 } else if (name.equalsIgnoreCase("height")) {
732 mHeight = parseSubstring(value, 0, 0);
734 //Log.v(TAG, "unknown tag: " + name + " (" + mProcessGenres + ")");
738 private boolean convertGenreCode(String input, String expected) {
739 String output = getGenreName(input);
740 if (output.equals(expected)) {
743 Log.d(TAG, "'" + input + "' -> '" + output + "', expected '" + expected + "'");
747 private void testGenreNameConverter() {
748 convertGenreCode("2", "Country");
749 convertGenreCode("(2)", "Country");
750 convertGenreCode("(2", "(2");
751 convertGenreCode("2 Foo", "Country");
752 convertGenreCode("(2) Foo", "Country");
753 convertGenreCode("(2 Foo", "(2 Foo");
754 convertGenreCode("2Foo", "2Foo");
755 convertGenreCode("(2)Foo", "Country");
756 convertGenreCode("200 Foo", "Foo");
757 convertGenreCode("(200) Foo", "Foo");
758 convertGenreCode("200Foo", "200Foo");
759 convertGenreCode("(200)Foo", "Foo");
760 convertGenreCode("200)Foo", "200)Foo");
761 convertGenreCode("200) Foo", "200) Foo");
764 public String getGenreName(String genreTagValue) {
766 if (genreTagValue == null) {
769 final int length = genreTagValue.length();
772 boolean parenthesized = false;
773 StringBuffer number = new StringBuffer();
775 for (; i < length; ++i) {
776 char c = genreTagValue.charAt(i);
777 if (i == 0 && c == '(') {
778 parenthesized = true;
779 } else if (Character.isDigit(c)) {
785 char charAfterNumber = i < length ? genreTagValue.charAt(i) : ' ';
786 if ((parenthesized && charAfterNumber == ')')
787 || !parenthesized && Character.isWhitespace(charAfterNumber)) {
789 short genreIndex = Short.parseShort(number.toString());
790 if (genreIndex >= 0) {
791 if (genreIndex < ID3_GENRES.length && ID3_GENRES[genreIndex] != null) {
792 return ID3_GENRES[genreIndex];
793 } else if (genreIndex == 0xFF) {
795 } else if (genreIndex < 0xFF && (i + 1) < length) {
796 // genre is valid but unknown,
797 // if there is a string after the value we take it
798 if (parenthesized && charAfterNumber == ')') {
801 String ret = genreTagValue.substring(i).trim();
802 if (ret.length() != 0) {
806 // else return the number, without parentheses
807 return number.toString();
810 } catch (NumberFormatException e) {
815 return genreTagValue;
818 private void processImageFile(String path) {
820 mBitmapOptions.outWidth = 0;
821 mBitmapOptions.outHeight = 0;
822 BitmapFactory.decodeFile(path, mBitmapOptions);
823 mWidth = mBitmapOptions.outWidth;
824 mHeight = mBitmapOptions.outHeight;
825 } catch (Throwable th) {
830 public void setMimeType(String mimeType) {
831 if ("audio/mp4".equals(mMimeType) &&
832 mimeType.startsWith("video")) {
833 // for feature parity with Donut, we force m4a files to keep the
834 // audio/mp4 mimetype, even if they are really "enhanced podcasts"
835 // with a video track
838 mMimeType = mimeType;
839 mFileType = MediaFile.getFileTypeForMimeType(mimeType);
843 * Formats the data into a values array suitable for use with the Media
846 * @return a map of values
848 private ContentValues toValues() {
849 ContentValues map = new ContentValues();
851 map.put(MediaStore.MediaColumns.DATA, mPath);
852 map.put(MediaStore.MediaColumns.TITLE, mTitle);
853 map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified);
854 map.put(MediaStore.MediaColumns.SIZE, mFileSize);
855 map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType);
856 map.put(MediaStore.MediaColumns.IS_DRM, mIsDrm);
858 String resolution = null;
859 if (mWidth > 0 && mHeight > 0) {
860 map.put(MediaStore.MediaColumns.WIDTH, mWidth);
861 map.put(MediaStore.MediaColumns.HEIGHT, mHeight);
862 resolution = mWidth + "x" + mHeight;
866 if (MediaFile.isVideoFileType(mFileType)) {
867 map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0
868 ? mArtist : MediaStore.UNKNOWN_STRING));
869 map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0
870 ? mAlbum : MediaStore.UNKNOWN_STRING));
871 map.put(Video.Media.DURATION, mDuration);
872 if (resolution != null) {
873 map.put(Video.Media.RESOLUTION, resolution);
876 map.put(Video.Media.DATE_TAKEN, mDate);
878 } else if (MediaFile.isImageFileType(mFileType)) {
879 // FIXME - add DESCRIPTION
880 } else if (MediaFile.isAudioFileType(mFileType)) {
881 map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ?
882 mArtist : MediaStore.UNKNOWN_STRING);
883 map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null &&
884 mAlbumArtist.length() > 0) ? mAlbumArtist : null);
885 map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ?
886 mAlbum : MediaStore.UNKNOWN_STRING);
887 map.put(Audio.Media.COMPOSER, mComposer);
888 map.put(Audio.Media.GENRE, mGenre);
890 map.put(Audio.Media.YEAR, mYear);
892 map.put(Audio.Media.TRACK, mTrack);
893 map.put(Audio.Media.DURATION, mDuration);
894 map.put(Audio.Media.COMPILATION, mCompilation);
900 private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
901 boolean alarms, boolean music, boolean podcasts)
902 throws RemoteException {
905 // use album artist if artist is missing
906 if (mArtist == null || mArtist.length() == 0) {
907 mArtist = mAlbumArtist;
910 ContentValues values = toValues();
911 String title = values.getAsString(MediaStore.MediaColumns.TITLE);
912 if (title == null || TextUtils.isEmpty(title.trim())) {
913 title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA));
914 values.put(MediaStore.MediaColumns.TITLE, title);
916 String album = values.getAsString(Audio.Media.ALBUM);
917 if (MediaStore.UNKNOWN_STRING.equals(album)) {
918 album = values.getAsString(MediaStore.MediaColumns.DATA);
919 // extract last path segment before file name
920 int lastSlash = album.lastIndexOf('/');
921 if (lastSlash >= 0) {
922 int previousSlash = 0;
924 int idx = album.indexOf('/', previousSlash + 1);
925 if (idx < 0 || idx >= lastSlash) {
930 if (previousSlash != 0) {
931 album = album.substring(previousSlash + 1, lastSlash);
932 values.put(Audio.Media.ALBUM, album);
936 long rowId = entry.mRowId;
937 if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) {
938 // Only set these for new entries. For existing entries, they
939 // may have been modified later, and we want to keep the current
940 // values so that custom ringtones still show up in the ringtone
942 values.put(Audio.Media.IS_RINGTONE, ringtones);
943 values.put(Audio.Media.IS_NOTIFICATION, notifications);
944 values.put(Audio.Media.IS_ALARM, alarms);
945 values.put(Audio.Media.IS_MUSIC, music);
946 values.put(Audio.Media.IS_PODCAST, podcasts);
947 } else if ((mFileType == MediaFile.FILE_TYPE_JPEG
948 || MediaFile.isRawImageFileType(mFileType)) && !mNoMedia) {
949 ExifInterface exif = null;
951 exif = new ExifInterface(entry.mPath);
952 } catch (IOException ex) {
956 float[] latlng = new float[2];
957 if (exif.getLatLong(latlng)) {
958 values.put(Images.Media.LATITUDE, latlng[0]);
959 values.put(Images.Media.LONGITUDE, latlng[1]);
962 long time = exif.getGpsDateTime();
964 values.put(Images.Media.DATE_TAKEN, time);
966 // If no time zone information is available, we should consider using
967 // EXIF local time as taken time if the difference between file time
968 // and EXIF local time is not less than 1 Day, otherwise MediaProvider
969 // will use file time as taken time.
970 time = exif.getDateTime();
971 if (time != -1 && Math.abs(mLastModified * 1000 - time) >= 86400000) {
972 values.put(Images.Media.DATE_TAKEN, time);
976 int orientation = exif.getAttributeInt(
977 ExifInterface.TAG_ORIENTATION, -1);
978 if (orientation != -1) {
979 // We only recognize a subset of orientation tag values.
981 switch(orientation) {
982 case ExifInterface.ORIENTATION_ROTATE_90:
985 case ExifInterface.ORIENTATION_ROTATE_180:
988 case ExifInterface.ORIENTATION_ROTATE_270:
995 values.put(Images.Media.ORIENTATION, degree);
1000 Uri tableUri = mFilesUri;
1001 MediaInserter inserter = mMediaInserter;
1003 if (MediaFile.isVideoFileType(mFileType)) {
1004 tableUri = mVideoUri;
1005 } else if (MediaFile.isImageFileType(mFileType)) {
1006 tableUri = mImagesUri;
1007 } else if (MediaFile.isAudioFileType(mFileType)) {
1008 tableUri = mAudioUri;
1012 boolean needToSetSettings = false;
1013 // Setting a flag in order not to use bulk insert for the file related with
1014 // notifications, ringtones, and alarms, because the rowId of the inserted file is
1016 if (notifications && !mDefaultNotificationSet) {
1017 if (TextUtils.isEmpty(mDefaultNotificationFilename) ||
1018 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) {
1019 needToSetSettings = true;
1021 } else if (ringtones && !mDefaultRingtoneSet) {
1022 if (TextUtils.isEmpty(mDefaultRingtoneFilename) ||
1023 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) {
1024 needToSetSettings = true;
1026 } else if (alarms && !mDefaultAlarmSet) {
1027 if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) ||
1028 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) {
1029 needToSetSettings = true;
1034 if (mMtpObjectHandle != 0) {
1035 values.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle);
1037 if (tableUri == mFilesUri) {
1038 int format = entry.mFormat;
1040 format = MediaFile.getFormatCode(entry.mPath, mMimeType);
1042 values.put(Files.FileColumns.FORMAT, format);
1044 // New file, insert it.
1045 // Directories need to be inserted before the files they contain, so they
1046 // get priority when bulk inserting.
1047 // If the rowId of the inserted file is needed, it gets inserted immediately,
1048 // bypassing the bulk inserter.
1049 if (inserter == null || needToSetSettings) {
1050 if (inserter != null) {
1051 inserter.flushAll();
1053 result = mMediaProvider.insert(tableUri, values);
1054 } else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) {
1055 inserter.insertwithPriority(tableUri, values);
1057 inserter.insert(tableUri, values);
1060 if (result != null) {
1061 rowId = ContentUris.parseId(result);
1062 entry.mRowId = rowId;
1066 result = ContentUris.withAppendedId(tableUri, rowId);
1067 // path should never change, and we want to avoid replacing mixed cased paths
1068 // with squashed lower case paths
1069 values.remove(MediaStore.MediaColumns.DATA);
1072 if (!MediaScanner.isNoMediaPath(entry.mPath)) {
1073 int fileType = MediaFile.getFileTypeForMimeType(mMimeType);
1074 if (MediaFile.isAudioFileType(fileType)) {
1075 mediaType = FileColumns.MEDIA_TYPE_AUDIO;
1076 } else if (MediaFile.isVideoFileType(fileType)) {
1077 mediaType = FileColumns.MEDIA_TYPE_VIDEO;
1078 } else if (MediaFile.isImageFileType(fileType)) {
1079 mediaType = FileColumns.MEDIA_TYPE_IMAGE;
1080 } else if (MediaFile.isPlayListFileType(fileType)) {
1081 mediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
1083 values.put(FileColumns.MEDIA_TYPE, mediaType);
1085 mMediaProvider.update(result, values, null, null);
1088 if(needToSetSettings) {
1089 if (notifications) {
1090 setRingtoneIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
1091 mDefaultNotificationSet = true;
1092 } else if (ringtones) {
1093 setRingtoneIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
1094 mDefaultRingtoneSet = true;
1095 } else if (alarms) {
1096 setRingtoneIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
1097 mDefaultAlarmSet = true;
1104 private boolean doesPathHaveFilename(String path, String filename) {
1105 int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1;
1106 int filenameLength = filename.length();
1107 return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) &&
1108 pathFilenameStart + filenameLength == path.length();
1111 private void setRingtoneIfNotSet(String settingName, Uri uri, long rowId) {
1112 if (wasRingtoneAlreadySet(settingName)) {
1116 ContentResolver cr = mContext.getContentResolver();
1117 String existingSettingValue = Settings.System.getString(cr, settingName);
1118 if (TextUtils.isEmpty(existingSettingValue)) {
1119 final Uri settingUri = Settings.System.getUriFor(settingName);
1120 final Uri ringtoneUri = ContentUris.withAppendedId(uri, rowId);
1121 RingtoneManager.setActualDefaultRingtoneUri(mContext,
1122 RingtoneManager.getDefaultType(settingUri), ringtoneUri);
1124 Settings.System.putInt(cr, settingSetIndicatorName(settingName), 1);
1127 private int getFileTypeFromDrm(String path) {
1128 if (!isDrmEnabled()) {
1132 int resultFileType = 0;
1134 if (mDrmManagerClient == null) {
1135 mDrmManagerClient = new DrmManagerClient(mContext);
1138 if (mDrmManagerClient.canHandle(path, null)) {
1140 String drmMimetype = mDrmManagerClient.getOriginalMimeType(path);
1141 if (drmMimetype != null) {
1142 mMimeType = drmMimetype;
1143 resultFileType = MediaFile.getFileTypeForMimeType(drmMimetype);
1146 return resultFileType;
1149 }; // end of anonymous MediaScannerClient instance
1151 private static boolean isSystemSoundWithMetadata(String path) {
1152 if (path.startsWith(SYSTEM_SOUNDS_DIR + ALARMS_DIR)
1153 || path.startsWith(SYSTEM_SOUNDS_DIR + RINGTONES_DIR)
1154 || path.startsWith(SYSTEM_SOUNDS_DIR + NOTIFICATIONS_DIR)) {
1160 private String settingSetIndicatorName(String base) {
1161 return base + "_set";
1164 private boolean wasRingtoneAlreadySet(String name) {
1165 ContentResolver cr = mContext.getContentResolver();
1166 String indicatorName = settingSetIndicatorName(name);
1168 return Settings.System.getInt(cr, indicatorName) != 0;
1169 } catch (SettingNotFoundException e) {
1174 private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
1176 String where = null;
1177 String[] selectionArgs = null;
1181 if (filePath != null) {
1182 // query for only one file
1183 where = MediaStore.Files.FileColumns._ID + ">?" +
1184 " AND " + Files.FileColumns.DATA + "=?";
1185 selectionArgs = new String[] { "", filePath };
1187 where = MediaStore.Files.FileColumns._ID + ">?";
1188 selectionArgs = new String[] { "" };
1191 mDefaultRingtoneSet = wasRingtoneAlreadySet(Settings.System.RINGTONE);
1192 mDefaultNotificationSet = wasRingtoneAlreadySet(Settings.System.NOTIFICATION_SOUND);
1193 mDefaultAlarmSet = wasRingtoneAlreadySet(Settings.System.ALARM_ALERT);
1195 // Tell the provider to not delete the file.
1196 // If the file is truly gone the delete is unnecessary, and we want to avoid
1197 // accidentally deleting files that are really there (this may happen if the
1198 // filesystem is mounted and unmounted while the scanner is running).
1199 Uri.Builder builder = mFilesUri.buildUpon();
1200 builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false");
1201 MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, builder.build());
1203 // Build the list of files from the content provider
1206 // First read existing files from the files table.
1207 // Because we'll be deleting entries for missing files as we go,
1208 // we need to query the database in small batches, to avoid problems
1209 // with CursorWindow positioning.
1210 long lastId = Long.MIN_VALUE;
1211 Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build();
1214 selectionArgs[0] = "" + lastId;
1219 c = mMediaProvider.query(limitUri, FILES_PRESCAN_PROJECTION,
1220 where, selectionArgs, MediaStore.Files.FileColumns._ID, null);
1225 int num = c.getCount();
1230 while (c.moveToNext()) {
1231 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
1232 String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
1233 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
1234 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
1237 // Only consider entries with absolute path names.
1238 // This allows storing URIs in the database without the
1239 // media scanner removing them.
1240 if (path != null && path.startsWith("/")) {
1241 boolean exists = false;
1243 exists = Os.access(path, android.system.OsConstants.F_OK);
1244 } catch (ErrnoException e1) {
1246 if (!exists && !MtpConstants.isAbstractObject(format)) {
1247 // do not delete missing playlists, since they may have been
1248 // modified by the user.
1249 // The user can delete them in the media player instead.
1250 // instead, clear the path and lastModified fields in the row
1251 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1252 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1254 if (!MediaFile.isPlayListFileType(fileType)) {
1255 deleter.delete(rowId);
1256 if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
1258 String parent = new File(path).getParent();
1259 mMediaProvider.call(MediaStore.UNHIDE_CALL, parent, null);
1275 // compute original size of images
1277 c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null, null);
1279 mOriginalCount = c.getCount();
1284 static class MediaBulkDeleter {
1285 StringBuilder whereClause = new StringBuilder();
1286 ArrayList<String> whereArgs = new ArrayList<String>(100);
1287 final ContentProviderClient mProvider;
1290 public MediaBulkDeleter(ContentProviderClient provider, Uri baseUri) {
1291 mProvider = provider;
1295 public void delete(long id) throws RemoteException {
1296 if (whereClause.length() != 0) {
1297 whereClause.append(",");
1299 whereClause.append("?");
1300 whereArgs.add("" + id);
1301 if (whereArgs.size() > 100) {
1305 public void flush() throws RemoteException {
1306 int size = whereArgs.size();
1308 String [] foo = new String [size];
1309 foo = whereArgs.toArray(foo);
1310 int numrows = mProvider.delete(mBaseUri,
1311 MediaStore.MediaColumns._ID + " IN (" +
1312 whereClause.toString() + ")", foo);
1313 //Log.i("@@@@@@@@@", "rows deleted: " + numrows);
1314 whereClause.setLength(0);
1320 private void postscan(final String[] directories) throws RemoteException {
1322 // handle playlists last, after we know what media files are on the storage.
1323 if (mProcessPlaylists) {
1327 // allow GC to clean up
1331 private void releaseResources() {
1332 // release the DrmManagerClient resources
1333 if (mDrmManagerClient != null) {
1334 mDrmManagerClient.close();
1335 mDrmManagerClient = null;
1339 public void scanDirectories(String[] directories) {
1341 long start = System.currentTimeMillis();
1342 prescan(null, true);
1343 long prescan = System.currentTimeMillis();
1345 if (ENABLE_BULK_INSERTS) {
1346 // create MediaInserter for bulk inserts
1347 mMediaInserter = new MediaInserter(mMediaProvider, 500);
1350 for (int i = 0; i < directories.length; i++) {
1351 processDirectory(directories[i], mClient);
1354 if (ENABLE_BULK_INSERTS) {
1355 // flush remaining inserts
1356 mMediaInserter.flushAll();
1357 mMediaInserter = null;
1360 long scan = System.currentTimeMillis();
1361 postscan(directories);
1362 long end = System.currentTimeMillis();
1365 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n");
1366 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n");
1367 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n");
1368 Log.d(TAG, " total time: " + (end - start) + "ms\n");
1370 } catch (SQLException e) {
1371 // this might happen if the SD card is removed while the media scanner is running
1372 Log.e(TAG, "SQLException in MediaScanner.scan()", e);
1373 } catch (UnsupportedOperationException e) {
1374 // this might happen if the SD card is removed while the media scanner is running
1375 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e);
1376 } catch (RemoteException e) {
1377 Log.e(TAG, "RemoteException in MediaScanner.scan()", e);
1383 // this function is used to scan a single file
1384 public Uri scanSingleFile(String path, String mimeType) {
1386 prescan(path, true);
1388 File file = new File(path);
1389 if (!file.exists()) {
1393 // lastModified is in milliseconds on Files.
1394 long lastModifiedSeconds = file.lastModified() / 1000;
1396 // always scan the file, so we can return the content://media Uri for existing files
1397 return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(),
1398 false, true, MediaScanner.isNoMediaPath(path));
1399 } catch (RemoteException e) {
1400 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1407 private static boolean isNoMediaFile(String path) {
1408 File file = new File(path);
1409 if (file.isDirectory()) return false;
1411 // special case certain file names
1412 // I use regionMatches() instead of substring() below
1413 // to avoid memory allocation
1414 int lastSlash = path.lastIndexOf('/');
1415 if (lastSlash >= 0 && lastSlash + 2 < path.length()) {
1416 // ignore those ._* files created by MacOS
1417 if (path.regionMatches(lastSlash + 1, "._", 0, 2)) {
1421 // ignore album art files created by Windows Media Player:
1422 // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg
1423 // and AlbumArt_{...}_Small.jpg
1424 if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) {
1425 if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) ||
1426 path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) {
1429 int length = path.length() - lastSlash - 1;
1430 if ((length == 17 && path.regionMatches(
1431 true, lastSlash + 1, "AlbumArtSmall", 0, 13)) ||
1433 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) {
1441 private static HashMap<String,String> mNoMediaPaths = new HashMap<String,String>();
1442 private static HashMap<String,String> mMediaPaths = new HashMap<String,String>();
1444 /* MediaProvider calls this when a .nomedia file is added or removed */
1445 public static void clearMediaPathCache(boolean clearMediaPaths, boolean clearNoMediaPaths) {
1446 synchronized (MediaScanner.class) {
1447 if (clearMediaPaths) {
1448 mMediaPaths.clear();
1450 if (clearNoMediaPaths) {
1451 mNoMediaPaths.clear();
1456 public static boolean isNoMediaPath(String path) {
1460 // return true if file or any parent directory has name starting with a dot
1461 if (path.indexOf("/.") >= 0) {
1465 int firstSlash = path.lastIndexOf('/');
1466 if (firstSlash <= 0) {
1469 String parent = path.substring(0, firstSlash);
1471 synchronized (MediaScanner.class) {
1472 if (mNoMediaPaths.containsKey(parent)) {
1474 } else if (!mMediaPaths.containsKey(parent)) {
1475 // check to see if any parent directories have a ".nomedia" file
1476 // start from 1 so we don't bother checking in the root directory
1478 while (offset >= 0) {
1479 int slashIndex = path.indexOf('/', offset);
1480 if (slashIndex > offset) {
1481 slashIndex++; // move past slash
1482 File file = new File(path.substring(0, slashIndex) + ".nomedia");
1483 if (file.exists()) {
1484 // we have a .nomedia in one of the parent directories
1485 mNoMediaPaths.put(parent, "");
1489 offset = slashIndex;
1491 mMediaPaths.put(parent, "");
1495 return isNoMediaFile(path);
1498 public void scanMtpFile(String path, int objectHandle, int format) {
1499 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1500 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1501 File file = new File(path);
1502 long lastModifiedSeconds = file.lastModified() / 1000;
1504 if (!MediaFile.isAudioFileType(fileType) && !MediaFile.isVideoFileType(fileType) &&
1505 !MediaFile.isImageFileType(fileType) && !MediaFile.isPlayListFileType(fileType) &&
1506 !MediaFile.isDrmFileType(fileType)) {
1508 // no need to use the media scanner, but we need to update last modified and file size
1509 ContentValues values = new ContentValues();
1510 values.put(Files.FileColumns.SIZE, file.length());
1511 values.put(Files.FileColumns.DATE_MODIFIED, lastModifiedSeconds);
1513 String[] whereArgs = new String[] { Integer.toString(objectHandle) };
1514 mMediaProvider.update(Files.getMtpObjectsUri(mVolumeName), values,
1515 "_id=?", whereArgs);
1516 } catch (RemoteException e) {
1517 Log.e(TAG, "RemoteException in scanMtpFile", e);
1522 mMtpObjectHandle = objectHandle;
1523 Cursor fileList = null;
1525 if (MediaFile.isPlayListFileType(fileType)) {
1526 // build file cache so we can look up tracks in the playlist
1527 prescan(null, true);
1529 FileEntry entry = makeEntryFor(path);
1530 if (entry != null) {
1531 fileList = mMediaProvider.query(mFilesUri,
1532 FILES_PRESCAN_PROJECTION, null, null, null, null);
1533 processPlayList(entry, fileList);
1536 // MTP will create a file entry for us so we don't want to do it in prescan
1537 prescan(path, false);
1539 // always scan the file, so we can return the content://media Uri for existing files
1540 mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(),
1541 (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path));
1543 } catch (RemoteException e) {
1544 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1546 mMtpObjectHandle = 0;
1547 if (fileList != null) {
1554 FileEntry makeEntryFor(String path) {
1556 String[] selectionArgs;
1560 where = Files.FileColumns.DATA + "=?";
1561 selectionArgs = new String[] { path };
1562 c = mMediaProvider.query(mFilesUriNoNotify, FILES_PRESCAN_PROJECTION,
1563 where, selectionArgs, null, null);
1564 if (c.moveToFirst()) {
1565 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
1566 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
1567 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
1568 return new FileEntry(rowId, path, lastModified, format);
1570 } catch (RemoteException e) {
1579 // returns the number of matching file/directory names, starting from the right
1580 private int matchPaths(String path1, String path2) {
1582 int end1 = path1.length();
1583 int end2 = path2.length();
1585 while (end1 > 0 && end2 > 0) {
1586 int slash1 = path1.lastIndexOf('/', end1 - 1);
1587 int slash2 = path2.lastIndexOf('/', end2 - 1);
1588 int backSlash1 = path1.lastIndexOf('\\', end1 - 1);
1589 int backSlash2 = path2.lastIndexOf('\\', end2 - 1);
1590 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1);
1591 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2);
1592 if (start1 < 0) start1 = 0; else start1++;
1593 if (start2 < 0) start2 = 0; else start2++;
1594 int length = end1 - start1;
1595 if (end2 - start2 != length) break;
1596 if (path1.regionMatches(true, start1, path2, start2, length)) {
1606 private boolean matchEntries(long rowId, String data) {
1608 int len = mPlaylistEntries.size();
1609 boolean done = true;
1610 for (int i = 0; i < len; i++) {
1611 PlaylistEntry entry = mPlaylistEntries.get(i);
1612 if (entry.bestmatchlevel == Integer.MAX_VALUE) {
1613 continue; // this entry has been matched already
1616 if (data.equalsIgnoreCase(entry.path)) {
1617 entry.bestmatchid = rowId;
1618 entry.bestmatchlevel = Integer.MAX_VALUE;
1619 continue; // no need for path matching
1622 int matchLength = matchPaths(data, entry.path);
1623 if (matchLength > entry.bestmatchlevel) {
1624 entry.bestmatchid = rowId;
1625 entry.bestmatchlevel = matchLength;
1631 private void cachePlaylistEntry(String line, String playListDirectory) {
1632 PlaylistEntry entry = new PlaylistEntry();
1633 // watch for trailing whitespace
1634 int entryLength = line.length();
1635 while (entryLength > 0 && Character.isWhitespace(line.charAt(entryLength - 1))) entryLength--;
1636 // path should be longer than 3 characters.
1637 // avoid index out of bounds errors below by returning here.
1638 if (entryLength < 3) return;
1639 if (entryLength < line.length()) line = line.substring(0, entryLength);
1641 // does entry appear to be an absolute path?
1642 // look for Unix or DOS absolute paths
1643 char ch1 = line.charAt(0);
1644 boolean fullPath = (ch1 == '/' ||
1645 (Character.isLetter(ch1) && line.charAt(1) == ':' && line.charAt(2) == '\\'));
1646 // if we have a relative path, combine entry with playListDirectory
1648 line = playListDirectory + line;
1650 //FIXME - should we look for "../" within the path?
1652 mPlaylistEntries.add(entry);
1655 private void processCachedPlaylist(Cursor fileList, ContentValues values, Uri playlistUri) {
1656 fileList.moveToPosition(-1);
1657 while (fileList.moveToNext()) {
1658 long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
1659 String data = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
1660 if (matchEntries(rowId, data)) {
1665 int len = mPlaylistEntries.size();
1667 for (int i = 0; i < len; i++) {
1668 PlaylistEntry entry = mPlaylistEntries.get(i);
1669 if (entry.bestmatchlevel > 0) {
1672 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index));
1673 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(entry.bestmatchid));
1674 mMediaProvider.insert(playlistUri, values);
1676 } catch (RemoteException e) {
1677 Log.e(TAG, "RemoteException in MediaScanner.processCachedPlaylist()", e);
1682 mPlaylistEntries.clear();
1685 private void processM3uPlayList(String path, String playListDirectory, Uri uri,
1686 ContentValues values, Cursor fileList) {
1687 BufferedReader reader = null;
1689 File f = new File(path);
1691 reader = new BufferedReader(
1692 new InputStreamReader(new FileInputStream(f)), 8192);
1693 String line = reader.readLine();
1694 mPlaylistEntries.clear();
1695 while (line != null) {
1696 // ignore comment lines, which begin with '#'
1697 if (line.length() > 0 && line.charAt(0) != '#') {
1698 cachePlaylistEntry(line, playListDirectory);
1700 line = reader.readLine();
1703 processCachedPlaylist(fileList, values, uri);
1705 } catch (IOException e) {
1706 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1711 } catch (IOException e) {
1712 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1717 private void processPlsPlayList(String path, String playListDirectory, Uri uri,
1718 ContentValues values, Cursor fileList) {
1719 BufferedReader reader = null;
1721 File f = new File(path);
1723 reader = new BufferedReader(
1724 new InputStreamReader(new FileInputStream(f)), 8192);
1725 String line = reader.readLine();
1726 mPlaylistEntries.clear();
1727 while (line != null) {
1728 // ignore comment lines, which begin with '#'
1729 if (line.startsWith("File")) {
1730 int equals = line.indexOf('=');
1732 cachePlaylistEntry(line.substring(equals + 1), playListDirectory);
1735 line = reader.readLine();
1738 processCachedPlaylist(fileList, values, uri);
1740 } catch (IOException e) {
1741 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1746 } catch (IOException e) {
1747 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1752 class WplHandler implements ElementListener {
1754 final ContentHandler handler;
1755 String playListDirectory;
1757 public WplHandler(String playListDirectory, Uri uri, Cursor fileList) {
1758 this.playListDirectory = playListDirectory;
1760 RootElement root = new RootElement("smil");
1761 Element body = root.getChild("body");
1762 Element seq = body.getChild("seq");
1763 Element media = seq.getChild("media");
1764 media.setElementListener(this);
1766 this.handler = root.getContentHandler();
1770 public void start(Attributes attributes) {
1771 String path = attributes.getValue("", "src");
1773 cachePlaylistEntry(path, playListDirectory);
1781 ContentHandler getContentHandler() {
1786 private void processWplPlayList(String path, String playListDirectory, Uri uri,
1787 ContentValues values, Cursor fileList) {
1788 FileInputStream fis = null;
1790 File f = new File(path);
1792 fis = new FileInputStream(f);
1794 mPlaylistEntries.clear();
1795 Xml.parse(fis, Xml.findEncodingByName("UTF-8"),
1796 new WplHandler(playListDirectory, uri, fileList).getContentHandler());
1798 processCachedPlaylist(fileList, values, uri);
1800 } catch (SAXException e) {
1801 e.printStackTrace();
1802 } catch (IOException e) {
1803 e.printStackTrace();
1808 } catch (IOException e) {
1809 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e);
1814 private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException {
1815 String path = entry.mPath;
1816 ContentValues values = new ContentValues();
1817 int lastSlash = path.lastIndexOf('/');
1818 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path);
1819 Uri uri, membersUri;
1820 long rowId = entry.mRowId;
1822 // make sure we have a name
1823 String name = values.getAsString(MediaStore.Audio.Playlists.NAME);
1825 name = values.getAsString(MediaStore.MediaColumns.TITLE);
1827 // extract name from file name
1828 int lastDot = path.lastIndexOf('.');
1829 name = (lastDot < 0 ? path.substring(lastSlash + 1)
1830 : path.substring(lastSlash + 1, lastDot));
1834 values.put(MediaStore.Audio.Playlists.NAME, name);
1835 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1838 values.put(MediaStore.Audio.Playlists.DATA, path);
1839 uri = mMediaProvider.insert(mPlaylistsUri, values);
1840 rowId = ContentUris.parseId(uri);
1841 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1843 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
1844 mMediaProvider.update(uri, values, null, null);
1846 // delete members of existing playlist
1847 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1848 mMediaProvider.delete(membersUri, null, null);
1851 String playListDirectory = path.substring(0, lastSlash + 1);
1852 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1853 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1855 if (fileType == MediaFile.FILE_TYPE_M3U) {
1856 processM3uPlayList(path, playListDirectory, membersUri, values, fileList);
1857 } else if (fileType == MediaFile.FILE_TYPE_PLS) {
1858 processPlsPlayList(path, playListDirectory, membersUri, values, fileList);
1859 } else if (fileType == MediaFile.FILE_TYPE_WPL) {
1860 processWplPlayList(path, playListDirectory, membersUri, values, fileList);
1864 private void processPlayLists() throws RemoteException {
1865 Iterator<FileEntry> iterator = mPlayLists.iterator();
1866 Cursor fileList = null;
1868 // use the files uri and projection because we need the format column,
1869 // but restrict the query to just audio files
1870 fileList = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION,
1871 "media_type=2", null, null, null);
1872 while (iterator.hasNext()) {
1873 FileEntry entry = iterator.next();
1874 // only process playlist files if they are new or have been modified since the last scan
1875 if (entry.mLastModifiedChanged) {
1876 processPlayList(entry, fileList);
1879 } catch (RemoteException e1) {
1881 if (fileList != null) {
1887 private native void processDirectory(String path, MediaScannerClient client);
1888 private native void processFile(String path, String mimeType, MediaScannerClient client);
1889 private native void setLocale(String locale);
1891 public native byte[] extractAlbumArt(FileDescriptor fd);
1893 private static native final void native_init();
1894 private native final void native_setup();
1895 private native final void native_finalize();
1898 public void close() {
1899 mCloseGuard.close();
1900 if (mClosed.compareAndSet(false, true)) {
1901 mMediaProvider.close();
1907 protected void finalize() throws Throwable {
1909 mCloseGuard.warnIfOpen();