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.database.Cursor;
25 import android.database.SQLException;
26 import android.drm.DrmManagerClient;
27 import android.graphics.BitmapFactory;
28 import android.mtp.MtpConstants;
29 import android.net.Uri;
30 import android.os.Environment;
31 import android.os.RemoteException;
32 import android.os.SystemProperties;
33 import android.provider.MediaStore;
34 import android.provider.MediaStore.Audio;
35 import android.provider.MediaStore.Audio.Playlists;
36 import android.provider.MediaStore.Files;
37 import android.provider.MediaStore.Files.FileColumns;
38 import android.provider.MediaStore.Images;
39 import android.provider.MediaStore.Video;
40 import android.provider.Settings;
41 import android.provider.Settings.SettingNotFoundException;
42 import android.sax.Element;
43 import android.sax.ElementListener;
44 import android.sax.RootElement;
45 import android.system.ErrnoException;
46 import android.system.Os;
47 import android.text.TextUtils;
48 import android.util.Log;
49 import android.util.Xml;
51 import dalvik.system.CloseGuard;
53 import org.xml.sax.Attributes;
54 import org.xml.sax.ContentHandler;
55 import org.xml.sax.SAXException;
57 import java.io.BufferedReader;
59 import java.io.FileDescriptor;
60 import java.io.FileInputStream;
61 import java.io.IOException;
62 import java.io.InputStreamReader;
63 import java.text.SimpleDateFormat;
64 import java.text.ParseException;
65 import java.util.ArrayList;
66 import java.util.HashMap;
67 import java.util.HashSet;
68 import java.util.Iterator;
69 import java.util.Locale;
70 import java.util.TimeZone;
71 import java.util.concurrent.atomic.AtomicBoolean;
74 * Internal service helper that no-one should use directly.
76 * The way the scan currently works is:
77 * - The Java MediaScannerService creates a MediaScanner (this class), and calls
78 * MediaScanner.scanDirectories on it.
79 * - scanDirectories() calls the native processDirectory() for each of the specified directories.
80 * - the processDirectory() JNI method wraps the provided mediascanner client in a native
81 * 'MyMediaScannerClient' class, then calls processDirectory() on the native MediaScanner
82 * object (which got created when the Java MediaScanner was created).
83 * - native MediaScanner.processDirectory() calls
84 * doProcessDirectory(), which recurses over the folder, and calls
85 * native MyMediaScannerClient.scanFile() for every file whose extension matches.
86 * - native MyMediaScannerClient.scanFile() calls back on Java MediaScannerClient.scanFile,
87 * which calls doScanFile, which after some setup calls back down to native code, calling
88 * MediaScanner.processFile().
89 * - MediaScanner.processFile() calls one of several methods, depending on the type of the
90 * file: parseMP3, parseMP4, parseMidi, parseOgg or parseWMA.
91 * - each of these methods gets metadata key/value pairs from the file, and repeatedly
92 * calls native MyMediaScannerClient.handleStringTag, which calls back up to its Java
93 * counterparts in this file.
94 * - Java handleStringTag() gathers the key/value pairs that it's interested in.
95 * - once processFile returns and we're back in Java code in doScanFile(), it calls
96 * Java MyMediaScannerClient.endFile(), which takes all the data that's been
97 * gathered and inserts an entry in to the database.
100 * Java MediaScannerService calls
101 * Java MediaScanner scanDirectories, which calls
102 * Java MediaScanner processDirectory (native method), which calls
103 * native MediaScanner processDirectory, which calls
104 * native MyMediaScannerClient scanFile, which calls
105 * Java MyMediaScannerClient scanFile, which calls
106 * Java MediaScannerClient doScanFile, which calls
107 * Java MediaScanner processFile (native method), which calls
108 * native MediaScanner processFile, which calls
109 * native parseMP3, parseMP4, parseMidi, parseOgg or parseWMA, which calls
110 * native MyMediaScanner handleStringTag, which calls
111 * Java MyMediaScanner handleStringTag.
112 * Once MediaScanner processFile returns, an entry is inserted in to the database.
114 * The MediaScanner class is not thread-safe, so it should only be used in a single threaded manner.
118 public class MediaScanner implements AutoCloseable {
120 System.loadLibrary("media_jni");
124 private final static String TAG = "MediaScanner";
126 private static final String[] FILES_PRESCAN_PROJECTION = new String[] {
127 Files.FileColumns._ID, // 0
128 Files.FileColumns.DATA, // 1
129 Files.FileColumns.FORMAT, // 2
130 Files.FileColumns.DATE_MODIFIED, // 3
133 private static final String[] ID_PROJECTION = new String[] {
134 Files.FileColumns._ID,
137 private static final int FILES_PRESCAN_ID_COLUMN_INDEX = 0;
138 private static final int FILES_PRESCAN_PATH_COLUMN_INDEX = 1;
139 private static final int FILES_PRESCAN_FORMAT_COLUMN_INDEX = 2;
140 private static final int FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX = 3;
142 private static final String[] PLAYLIST_MEMBERS_PROJECTION = new String[] {
143 Audio.Playlists.Members.PLAYLIST_ID, // 0
146 private static final int ID_PLAYLISTS_COLUMN_INDEX = 0;
147 private static final int PATH_PLAYLISTS_COLUMN_INDEX = 1;
148 private static final int DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX = 2;
150 private static final String RINGTONES_DIR = "/ringtones/";
151 private static final String NOTIFICATIONS_DIR = "/notifications/";
152 private static final String ALARMS_DIR = "/alarms/";
153 private static final String MUSIC_DIR = "/music/";
154 private static final String PODCAST_DIR = "/podcasts/";
156 private static final String[] ID3_GENRES = {
238 // The following genres are Winamp extensions
285 // The following ones seem to be fairly widely supported as well
300 "Contemporary Christian",
308 // 148 and up don't seem to have been defined yet.
311 private long mNativeContext;
312 private final Context mContext;
313 private final String mPackageName;
314 private final String mVolumeName;
315 private final ContentProviderClient mMediaProvider;
316 private final Uri mAudioUri;
317 private final Uri mVideoUri;
318 private final Uri mImagesUri;
319 private final Uri mThumbsUri;
320 private final Uri mPlaylistsUri;
321 private final Uri mFilesUri;
322 private final Uri mFilesUriNoNotify;
323 private final boolean mProcessPlaylists;
324 private final boolean mProcessGenres;
325 private int mMtpObjectHandle;
327 private final AtomicBoolean mClosed = new AtomicBoolean();
328 private final CloseGuard mCloseGuard = CloseGuard.get();
330 /** whether to use bulk inserts or individual inserts for each item */
331 private static final boolean ENABLE_BULK_INSERTS = true;
333 // used when scanning the image database so we know whether we have to prune
334 // old thumbnail files
335 private int mOriginalCount;
336 /** Whether the scanner has set a default sound for the ringer ringtone. */
337 private boolean mDefaultRingtoneSet;
338 /** Whether the scanner has set a default sound for the notification ringtone. */
339 private boolean mDefaultNotificationSet;
340 /** Whether the scanner has set a default sound for the alarm ringtone. */
341 private boolean mDefaultAlarmSet;
342 /** The filename for the default sound for the ringer ringtone. */
343 private String mDefaultRingtoneFilename;
344 /** The filename for the default sound for the notification ringtone. */
345 private String mDefaultNotificationFilename;
346 /** The filename for the default sound for the alarm ringtone. */
347 private String mDefaultAlarmAlertFilename;
349 * The prefix for system properties that define the default sound for
350 * ringtones. Concatenate the name of the setting from Settings
351 * to get the full system property.
353 private static final String DEFAULT_RINGTONE_PROPERTY_PREFIX = "ro.config.";
355 private final BitmapFactory.Options mBitmapOptions = new BitmapFactory.Options();
357 private static class FileEntry {
362 boolean mLastModifiedChanged;
364 FileEntry(long rowId, String path, long lastModified, int format) {
367 mLastModified = lastModified;
369 mLastModifiedChanged = false;
373 public String toString() {
374 return mPath + " mRowId: " + mRowId;
378 private static class PlaylistEntry {
384 private final ArrayList<PlaylistEntry> mPlaylistEntries = new ArrayList<>();
385 private final ArrayList<FileEntry> mPlayLists = new ArrayList<>();
387 private MediaInserter mMediaInserter;
389 private DrmManagerClient mDrmManagerClient = null;
391 public MediaScanner(Context c, String volumeName) {
394 mPackageName = c.getPackageName();
395 mVolumeName = volumeName;
397 mBitmapOptions.inSampleSize = 1;
398 mBitmapOptions.inJustDecodeBounds = true;
400 setDefaultRingtoneFileNames();
402 mMediaProvider = mContext.getContentResolver()
403 .acquireContentProviderClient(MediaStore.AUTHORITY);
405 mAudioUri = Audio.Media.getContentUri(volumeName);
406 mVideoUri = Video.Media.getContentUri(volumeName);
407 mImagesUri = Images.Media.getContentUri(volumeName);
408 mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
409 mFilesUri = Files.getContentUri(volumeName);
410 mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build();
412 if (!volumeName.equals("internal")) {
413 // we only support playlists on external media
414 mProcessPlaylists = true;
415 mProcessGenres = true;
416 mPlaylistsUri = Playlists.getContentUri(volumeName);
418 mProcessPlaylists = false;
419 mProcessGenres = false;
420 mPlaylistsUri = null;
423 final Locale locale = mContext.getResources().getConfiguration().locale;
424 if (locale != null) {
425 String language = locale.getLanguage();
426 String country = locale.getCountry();
427 if (language != null) {
428 if (country != null) {
429 setLocale(language + "_" + country);
436 mCloseGuard.open("close");
439 private void setDefaultRingtoneFileNames() {
440 mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
441 + Settings.System.RINGTONE);
442 mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
443 + Settings.System.NOTIFICATION_SOUND);
444 mDefaultAlarmAlertFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
445 + Settings.System.ALARM_ALERT);
448 private final MyMediaScannerClient mClient = new MyMediaScannerClient();
450 private boolean isDrmEnabled() {
451 String prop = SystemProperties.get("drm.service.enabled");
452 return prop != null && prop.equals("true");
455 private class MyMediaScannerClient implements MediaScannerClient {
457 private final SimpleDateFormat mDateFormatter;
459 private String mArtist;
460 private String mAlbumArtist; // use this if mArtist is missing
461 private String mAlbum;
462 private String mTitle;
463 private String mComposer;
464 private String mGenre;
465 private String mMimeType;
466 private int mFileType;
469 private int mDuration;
470 private String mPath;
472 private long mLastModified;
473 private long mFileSize;
474 private String mWriter;
475 private int mCompilation;
476 private boolean mIsDrm;
477 private boolean mNoMedia; // flag to suppress file from appearing in media tables
481 public MyMediaScannerClient() {
482 mDateFormatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
483 mDateFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
486 public FileEntry beginFile(String path, String mimeType, long lastModified,
487 long fileSize, boolean isDirectory, boolean noMedia) {
488 mMimeType = mimeType;
490 mFileSize = fileSize;
494 if (!noMedia && isNoMediaFile(path)) {
499 // try mimeType first, if it is specified
500 if (mimeType != null) {
501 mFileType = MediaFile.getFileTypeForMimeType(mimeType);
504 // if mimeType was not specified, compute file type based on file extension.
505 if (mFileType == 0) {
506 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
507 if (mediaFileType != null) {
508 mFileType = mediaFileType.fileType;
509 if (mMimeType == null) {
510 mMimeType = mediaFileType.mimeType;
515 if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) {
516 mFileType = getFileTypeFromDrm(path);
520 FileEntry entry = makeEntryFor(path);
521 // add some slack to avoid a rounding error
522 long delta = (entry != null) ? (lastModified - entry.mLastModified) : 0;
523 boolean wasModified = delta > 1 || delta < -1;
524 if (entry == null || wasModified) {
526 entry.mLastModified = lastModified;
528 entry = new FileEntry(0, path, lastModified,
529 (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
531 entry.mLastModifiedChanged = true;
534 if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
535 mPlayLists.add(entry);
536 // we don't process playlists in the main scan, so return null
540 // clear all the metadata
552 mLastModified = lastModified;
562 public void scanFile(String path, long lastModified, long fileSize,
563 boolean isDirectory, boolean noMedia) {
564 // This is the callback funtion from native codes.
565 // Log.v(TAG, "scanFile: "+path);
566 doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
569 public Uri doScanFile(String path, String mimeType, long lastModified,
570 long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
572 // long t1 = System.currentTimeMillis();
574 FileEntry entry = beginFile(path, mimeType, lastModified,
575 fileSize, isDirectory, noMedia);
581 // if this file was just inserted via mtp, set the rowid to zero
582 // (even though it already exists in the database), to trigger
583 // the correct code path for updating its entry
584 if (mMtpObjectHandle != 0) {
588 if (entry.mPath != null &&
589 ((!mDefaultNotificationSet &&
590 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename))
591 || (!mDefaultRingtoneSet &&
592 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename))
593 || (!mDefaultAlarmSet &&
594 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)))) {
595 Log.w(TAG, "forcing rescan of " + entry.mPath +
596 "since ringtone setting didn't finish");
600 // rescan for metadata if file was modified since last scan
601 if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
603 result = endFile(entry, false, false, false, false, false);
605 String lowpath = path.toLowerCase(Locale.ROOT);
606 boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
607 boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
608 boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
609 boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
610 boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
611 (!ringtones && !notifications && !alarms && !podcasts);
613 boolean isaudio = MediaFile.isAudioFileType(mFileType);
614 boolean isvideo = MediaFile.isVideoFileType(mFileType);
615 boolean isimage = MediaFile.isImageFileType(mFileType);
617 if (isaudio || isvideo || isimage) {
618 path = Environment.maybeTranslateEmulatedPathToInternal(new File(path))
622 // we only extract metadata for audio and video files
623 if (isaudio || isvideo) {
624 processFile(path, mimeType, this);
628 processImageFile(path);
631 result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
634 } catch (RemoteException e) {
635 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
637 // long t2 = System.currentTimeMillis();
638 // Log.v(TAG, "scanFile: " + path + " took " + (t2-t1));
642 private long parseDate(String date) {
644 return mDateFormatter.parse(date).getTime();
645 } catch (ParseException e) {
650 private int parseSubstring(String s, int start, int defaultValue) {
651 int length = s.length();
652 if (start == length) return defaultValue;
654 char ch = s.charAt(start++);
655 // return defaultValue if we have no integer at all
656 if (ch < '0' || ch > '9') return defaultValue;
658 int result = ch - '0';
659 while (start < length) {
660 ch = s.charAt(start++);
661 if (ch < '0' || ch > '9') return result;
662 result = result * 10 + (ch - '0');
668 public void handleStringTag(String name, String value) {
669 if (name.equalsIgnoreCase("title") || name.startsWith("title;")) {
670 // Don't trim() here, to preserve the special \001 character
671 // used to force sorting. The media provider will trim() before
672 // inserting the title in to the database.
674 } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) {
675 mArtist = value.trim();
676 } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")
677 || name.equalsIgnoreCase("band") || name.startsWith("band;")) {
678 mAlbumArtist = value.trim();
679 } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) {
680 mAlbum = value.trim();
681 } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) {
682 mComposer = value.trim();
683 } else if (mProcessGenres &&
684 (name.equalsIgnoreCase("genre") || name.startsWith("genre;"))) {
685 mGenre = getGenreName(value);
686 } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) {
687 mYear = parseSubstring(value, 0, 0);
688 } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) {
689 // track number might be of the form "2/12"
690 // we just read the number before the slash
691 int num = parseSubstring(value, 0, 0);
692 mTrack = (mTrack / 1000) * 1000 + num;
693 } else if (name.equalsIgnoreCase("discnumber") ||
694 name.equals("set") || name.startsWith("set;")) {
695 // set number might be of the form "1/3"
696 // we just read the number before the slash
697 int num = parseSubstring(value, 0, 0);
698 mTrack = (num * 1000) + (mTrack % 1000);
699 } else if (name.equalsIgnoreCase("duration")) {
700 mDuration = parseSubstring(value, 0, 0);
701 } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {
702 mWriter = value.trim();
703 } else if (name.equalsIgnoreCase("compilation")) {
704 mCompilation = parseSubstring(value, 0, 0);
705 } else if (name.equalsIgnoreCase("isdrm")) {
706 mIsDrm = (parseSubstring(value, 0, 0) == 1);
707 } else if (name.equalsIgnoreCase("date")) {
708 mDate = parseDate(value);
709 } else if (name.equalsIgnoreCase("width")) {
710 mWidth = parseSubstring(value, 0, 0);
711 } else if (name.equalsIgnoreCase("height")) {
712 mHeight = parseSubstring(value, 0, 0);
714 //Log.v(TAG, "unknown tag: " + name + " (" + mProcessGenres + ")");
718 private boolean convertGenreCode(String input, String expected) {
719 String output = getGenreName(input);
720 if (output.equals(expected)) {
723 Log.d(TAG, "'" + input + "' -> '" + output + "', expected '" + expected + "'");
727 private void testGenreNameConverter() {
728 convertGenreCode("2", "Country");
729 convertGenreCode("(2)", "Country");
730 convertGenreCode("(2", "(2");
731 convertGenreCode("2 Foo", "Country");
732 convertGenreCode("(2) Foo", "Country");
733 convertGenreCode("(2 Foo", "(2 Foo");
734 convertGenreCode("2Foo", "2Foo");
735 convertGenreCode("(2)Foo", "Country");
736 convertGenreCode("200 Foo", "Foo");
737 convertGenreCode("(200) Foo", "Foo");
738 convertGenreCode("200Foo", "200Foo");
739 convertGenreCode("(200)Foo", "Foo");
740 convertGenreCode("200)Foo", "200)Foo");
741 convertGenreCode("200) Foo", "200) Foo");
744 public String getGenreName(String genreTagValue) {
746 if (genreTagValue == null) {
749 final int length = genreTagValue.length();
752 boolean parenthesized = false;
753 StringBuffer number = new StringBuffer();
755 for (; i < length; ++i) {
756 char c = genreTagValue.charAt(i);
757 if (i == 0 && c == '(') {
758 parenthesized = true;
759 } else if (Character.isDigit(c)) {
765 char charAfterNumber = i < length ? genreTagValue.charAt(i) : ' ';
766 if ((parenthesized && charAfterNumber == ')')
767 || !parenthesized && Character.isWhitespace(charAfterNumber)) {
769 short genreIndex = Short.parseShort(number.toString());
770 if (genreIndex >= 0) {
771 if (genreIndex < ID3_GENRES.length && ID3_GENRES[genreIndex] != null) {
772 return ID3_GENRES[genreIndex];
773 } else if (genreIndex == 0xFF) {
775 } else if (genreIndex < 0xFF && (i + 1) < length) {
776 // genre is valid but unknown,
777 // if there is a string after the value we take it
778 if (parenthesized && charAfterNumber == ')') {
781 String ret = genreTagValue.substring(i).trim();
782 if (ret.length() != 0) {
786 // else return the number, without parentheses
787 return number.toString();
790 } catch (NumberFormatException e) {
795 return genreTagValue;
798 private void processImageFile(String path) {
800 mBitmapOptions.outWidth = 0;
801 mBitmapOptions.outHeight = 0;
802 BitmapFactory.decodeFile(path, mBitmapOptions);
803 mWidth = mBitmapOptions.outWidth;
804 mHeight = mBitmapOptions.outHeight;
805 } catch (Throwable th) {
810 public void setMimeType(String mimeType) {
811 if ("audio/mp4".equals(mMimeType) &&
812 mimeType.startsWith("video")) {
813 // for feature parity with Donut, we force m4a files to keep the
814 // audio/mp4 mimetype, even if they are really "enhanced podcasts"
815 // with a video track
818 mMimeType = mimeType;
819 mFileType = MediaFile.getFileTypeForMimeType(mimeType);
823 * Formats the data into a values array suitable for use with the Media
826 * @return a map of values
828 private ContentValues toValues() {
829 ContentValues map = new ContentValues();
831 map.put(MediaStore.MediaColumns.DATA, mPath);
832 map.put(MediaStore.MediaColumns.TITLE, mTitle);
833 map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified);
834 map.put(MediaStore.MediaColumns.SIZE, mFileSize);
835 map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType);
836 map.put(MediaStore.MediaColumns.IS_DRM, mIsDrm);
838 String resolution = null;
839 if (mWidth > 0 && mHeight > 0) {
840 map.put(MediaStore.MediaColumns.WIDTH, mWidth);
841 map.put(MediaStore.MediaColumns.HEIGHT, mHeight);
842 resolution = mWidth + "x" + mHeight;
846 if (MediaFile.isVideoFileType(mFileType)) {
847 map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0
848 ? mArtist : MediaStore.UNKNOWN_STRING));
849 map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0
850 ? mAlbum : MediaStore.UNKNOWN_STRING));
851 map.put(Video.Media.DURATION, mDuration);
852 if (resolution != null) {
853 map.put(Video.Media.RESOLUTION, resolution);
856 map.put(Video.Media.DATE_TAKEN, mDate);
858 } else if (MediaFile.isImageFileType(mFileType)) {
859 // FIXME - add DESCRIPTION
860 } else if (MediaFile.isAudioFileType(mFileType)) {
861 map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ?
862 mArtist : MediaStore.UNKNOWN_STRING);
863 map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null &&
864 mAlbumArtist.length() > 0) ? mAlbumArtist : null);
865 map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ?
866 mAlbum : MediaStore.UNKNOWN_STRING);
867 map.put(Audio.Media.COMPOSER, mComposer);
868 map.put(Audio.Media.GENRE, mGenre);
870 map.put(Audio.Media.YEAR, mYear);
872 map.put(Audio.Media.TRACK, mTrack);
873 map.put(Audio.Media.DURATION, mDuration);
874 map.put(Audio.Media.COMPILATION, mCompilation);
880 private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
881 boolean alarms, boolean music, boolean podcasts)
882 throws RemoteException {
885 // use album artist if artist is missing
886 if (mArtist == null || mArtist.length() == 0) {
887 mArtist = mAlbumArtist;
890 ContentValues values = toValues();
891 String title = values.getAsString(MediaStore.MediaColumns.TITLE);
892 if (title == null || TextUtils.isEmpty(title.trim())) {
893 title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA));
894 values.put(MediaStore.MediaColumns.TITLE, title);
896 String album = values.getAsString(Audio.Media.ALBUM);
897 if (MediaStore.UNKNOWN_STRING.equals(album)) {
898 album = values.getAsString(MediaStore.MediaColumns.DATA);
899 // extract last path segment before file name
900 int lastSlash = album.lastIndexOf('/');
901 if (lastSlash >= 0) {
902 int previousSlash = 0;
904 int idx = album.indexOf('/', previousSlash + 1);
905 if (idx < 0 || idx >= lastSlash) {
910 if (previousSlash != 0) {
911 album = album.substring(previousSlash + 1, lastSlash);
912 values.put(Audio.Media.ALBUM, album);
916 long rowId = entry.mRowId;
917 if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) {
918 // Only set these for new entries. For existing entries, they
919 // may have been modified later, and we want to keep the current
920 // values so that custom ringtones still show up in the ringtone
922 values.put(Audio.Media.IS_RINGTONE, ringtones);
923 values.put(Audio.Media.IS_NOTIFICATION, notifications);
924 values.put(Audio.Media.IS_ALARM, alarms);
925 values.put(Audio.Media.IS_MUSIC, music);
926 values.put(Audio.Media.IS_PODCAST, podcasts);
927 } else if ((mFileType == MediaFile.FILE_TYPE_JPEG
928 || MediaFile.isRawImageFileType(mFileType)) && !mNoMedia) {
929 ExifInterface exif = null;
931 exif = new ExifInterface(entry.mPath);
932 } catch (IOException ex) {
936 float[] latlng = new float[2];
937 if (exif.getLatLong(latlng)) {
938 values.put(Images.Media.LATITUDE, latlng[0]);
939 values.put(Images.Media.LONGITUDE, latlng[1]);
942 long time = exif.getGpsDateTime();
944 values.put(Images.Media.DATE_TAKEN, time);
946 // If no time zone information is available, we should consider using
947 // EXIF local time as taken time if the difference between file time
948 // and EXIF local time is not less than 1 Day, otherwise MediaProvider
949 // will use file time as taken time.
950 time = exif.getDateTime();
951 if (time != -1 && Math.abs(mLastModified * 1000 - time) >= 86400000) {
952 values.put(Images.Media.DATE_TAKEN, time);
956 int orientation = exif.getAttributeInt(
957 ExifInterface.TAG_ORIENTATION, -1);
958 if (orientation != -1) {
959 // We only recognize a subset of orientation tag values.
961 switch(orientation) {
962 case ExifInterface.ORIENTATION_ROTATE_90:
965 case ExifInterface.ORIENTATION_ROTATE_180:
968 case ExifInterface.ORIENTATION_ROTATE_270:
975 values.put(Images.Media.ORIENTATION, degree);
980 Uri tableUri = mFilesUri;
981 MediaInserter inserter = mMediaInserter;
983 if (MediaFile.isVideoFileType(mFileType)) {
984 tableUri = mVideoUri;
985 } else if (MediaFile.isImageFileType(mFileType)) {
986 tableUri = mImagesUri;
987 } else if (MediaFile.isAudioFileType(mFileType)) {
988 tableUri = mAudioUri;
992 boolean needToSetSettings = false;
993 // Setting a flag in order not to use bulk insert for the file related with
994 // notifications, ringtones, and alarms, because the rowId of the inserted file is
996 if (notifications && !mDefaultNotificationSet) {
997 if (TextUtils.isEmpty(mDefaultNotificationFilename) ||
998 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) {
999 needToSetSettings = true;
1001 } else if (ringtones && !mDefaultRingtoneSet) {
1002 if (TextUtils.isEmpty(mDefaultRingtoneFilename) ||
1003 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) {
1004 needToSetSettings = true;
1006 } else if (alarms && !mDefaultAlarmSet) {
1007 if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) ||
1008 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) {
1009 needToSetSettings = true;
1014 if (mMtpObjectHandle != 0) {
1015 values.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle);
1017 if (tableUri == mFilesUri) {
1018 int format = entry.mFormat;
1020 format = MediaFile.getFormatCode(entry.mPath, mMimeType);
1022 values.put(Files.FileColumns.FORMAT, format);
1024 // New file, insert it.
1025 // Directories need to be inserted before the files they contain, so they
1026 // get priority when bulk inserting.
1027 // If the rowId of the inserted file is needed, it gets inserted immediately,
1028 // bypassing the bulk inserter.
1029 if (inserter == null || needToSetSettings) {
1030 if (inserter != null) {
1031 inserter.flushAll();
1033 result = mMediaProvider.insert(tableUri, values);
1034 } else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) {
1035 inserter.insertwithPriority(tableUri, values);
1037 inserter.insert(tableUri, values);
1040 if (result != null) {
1041 rowId = ContentUris.parseId(result);
1042 entry.mRowId = rowId;
1046 result = ContentUris.withAppendedId(tableUri, rowId);
1047 // path should never change, and we want to avoid replacing mixed cased paths
1048 // with squashed lower case paths
1049 values.remove(MediaStore.MediaColumns.DATA);
1052 if (!MediaScanner.isNoMediaPath(entry.mPath)) {
1053 int fileType = MediaFile.getFileTypeForMimeType(mMimeType);
1054 if (MediaFile.isAudioFileType(fileType)) {
1055 mediaType = FileColumns.MEDIA_TYPE_AUDIO;
1056 } else if (MediaFile.isVideoFileType(fileType)) {
1057 mediaType = FileColumns.MEDIA_TYPE_VIDEO;
1058 } else if (MediaFile.isImageFileType(fileType)) {
1059 mediaType = FileColumns.MEDIA_TYPE_IMAGE;
1060 } else if (MediaFile.isPlayListFileType(fileType)) {
1061 mediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
1063 values.put(FileColumns.MEDIA_TYPE, mediaType);
1065 mMediaProvider.update(result, values, null, null);
1068 if(needToSetSettings) {
1069 if (notifications) {
1070 setRingtoneIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
1071 mDefaultNotificationSet = true;
1072 } else if (ringtones) {
1073 setRingtoneIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
1074 mDefaultRingtoneSet = true;
1075 } else if (alarms) {
1076 setRingtoneIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
1077 mDefaultAlarmSet = true;
1084 private boolean doesPathHaveFilename(String path, String filename) {
1085 int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1;
1086 int filenameLength = filename.length();
1087 return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) &&
1088 pathFilenameStart + filenameLength == path.length();
1091 private void setRingtoneIfNotSet(String settingName, Uri uri, long rowId) {
1092 if (wasRingtoneAlreadySet(settingName)) {
1096 ContentResolver cr = mContext.getContentResolver();
1097 String existingSettingValue = Settings.System.getString(cr, settingName);
1098 if (TextUtils.isEmpty(existingSettingValue)) {
1099 final Uri settingUri = Settings.System.getUriFor(settingName);
1100 final Uri ringtoneUri = ContentUris.withAppendedId(uri, rowId);
1101 RingtoneManager.setActualDefaultRingtoneUri(mContext,
1102 RingtoneManager.getDefaultType(settingUri), ringtoneUri);
1104 Settings.System.putInt(cr, settingSetIndicatorName(settingName), 1);
1107 private int getFileTypeFromDrm(String path) {
1108 if (!isDrmEnabled()) {
1112 int resultFileType = 0;
1114 if (mDrmManagerClient == null) {
1115 mDrmManagerClient = new DrmManagerClient(mContext);
1118 if (mDrmManagerClient.canHandle(path, null)) {
1120 String drmMimetype = mDrmManagerClient.getOriginalMimeType(path);
1121 if (drmMimetype != null) {
1122 mMimeType = drmMimetype;
1123 resultFileType = MediaFile.getFileTypeForMimeType(drmMimetype);
1126 return resultFileType;
1129 }; // end of anonymous MediaScannerClient instance
1131 private String settingSetIndicatorName(String base) {
1132 return base + "_set";
1135 private boolean wasRingtoneAlreadySet(String name) {
1136 ContentResolver cr = mContext.getContentResolver();
1137 String indicatorName = settingSetIndicatorName(name);
1139 return Settings.System.getInt(cr, indicatorName) != 0;
1140 } catch (SettingNotFoundException e) {
1145 private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
1147 String where = null;
1148 String[] selectionArgs = null;
1152 if (filePath != null) {
1153 // query for only one file
1154 where = MediaStore.Files.FileColumns._ID + ">?" +
1155 " AND " + Files.FileColumns.DATA + "=?";
1156 selectionArgs = new String[] { "", filePath };
1158 where = MediaStore.Files.FileColumns._ID + ">?";
1159 selectionArgs = new String[] { "" };
1162 mDefaultRingtoneSet = wasRingtoneAlreadySet(Settings.System.RINGTONE);
1163 mDefaultNotificationSet = wasRingtoneAlreadySet(Settings.System.NOTIFICATION_SOUND);
1164 mDefaultAlarmSet = wasRingtoneAlreadySet(Settings.System.ALARM_ALERT);
1166 // Tell the provider to not delete the file.
1167 // If the file is truly gone the delete is unnecessary, and we want to avoid
1168 // accidentally deleting files that are really there (this may happen if the
1169 // filesystem is mounted and unmounted while the scanner is running).
1170 Uri.Builder builder = mFilesUri.buildUpon();
1171 builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false");
1172 MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, builder.build());
1174 // Build the list of files from the content provider
1177 // First read existing files from the files table.
1178 // Because we'll be deleting entries for missing files as we go,
1179 // we need to query the database in small batches, to avoid problems
1180 // with CursorWindow positioning.
1181 long lastId = Long.MIN_VALUE;
1182 Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build();
1185 selectionArgs[0] = "" + lastId;
1190 c = mMediaProvider.query(limitUri, FILES_PRESCAN_PROJECTION,
1191 where, selectionArgs, MediaStore.Files.FileColumns._ID, null);
1196 int num = c.getCount();
1201 while (c.moveToNext()) {
1202 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
1203 String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
1204 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
1205 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
1208 // Only consider entries with absolute path names.
1209 // This allows storing URIs in the database without the
1210 // media scanner removing them.
1211 if (path != null && path.startsWith("/")) {
1212 boolean exists = false;
1214 exists = Os.access(path, android.system.OsConstants.F_OK);
1215 } catch (ErrnoException e1) {
1217 if (!exists && !MtpConstants.isAbstractObject(format)) {
1218 // do not delete missing playlists, since they may have been
1219 // modified by the user.
1220 // The user can delete them in the media player instead.
1221 // instead, clear the path and lastModified fields in the row
1222 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1223 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1225 if (!MediaFile.isPlayListFileType(fileType)) {
1226 deleter.delete(rowId);
1227 if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
1229 String parent = new File(path).getParent();
1230 mMediaProvider.call(MediaStore.UNHIDE_CALL, parent, null);
1246 // compute original size of images
1248 c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null, null);
1250 mOriginalCount = c.getCount();
1255 private boolean inScanDirectory(String path, String[] directories) {
1256 for (int i = 0; i < directories.length; i++) {
1257 String directory = directories[i];
1258 if (path.startsWith(directory)) {
1265 private void pruneDeadThumbnailFiles() {
1266 HashSet<String> existingFiles = new HashSet<String>();
1267 String directory = "/sdcard/DCIM/.thumbnails";
1268 String [] files = (new File(directory)).list();
1271 files = new String[0];
1273 for (int i = 0; i < files.length; i++) {
1274 String fullPathString = directory + "/" + files[i];
1275 existingFiles.add(fullPathString);
1279 c = mMediaProvider.query(
1281 new String [] { "_data" },
1285 Log.v(TAG, "pruneDeadThumbnailFiles... " + c);
1286 if (c != null && c.moveToFirst()) {
1288 String fullPathString = c.getString(0);
1289 existingFiles.remove(fullPathString);
1290 } while (c.moveToNext());
1293 for (String fileToDelete : existingFiles) {
1295 Log.v(TAG, "fileToDelete is " + fileToDelete);
1297 (new File(fileToDelete)).delete();
1298 } catch (SecurityException ex) {
1302 Log.v(TAG, "/pruneDeadThumbnailFiles... " + c);
1303 } catch (RemoteException e) {
1304 // We will soon be killed...
1312 static class MediaBulkDeleter {
1313 StringBuilder whereClause = new StringBuilder();
1314 ArrayList<String> whereArgs = new ArrayList<String>(100);
1315 final ContentProviderClient mProvider;
1318 public MediaBulkDeleter(ContentProviderClient provider, Uri baseUri) {
1319 mProvider = provider;
1323 public void delete(long id) throws RemoteException {
1324 if (whereClause.length() != 0) {
1325 whereClause.append(",");
1327 whereClause.append("?");
1328 whereArgs.add("" + id);
1329 if (whereArgs.size() > 100) {
1333 public void flush() throws RemoteException {
1334 int size = whereArgs.size();
1336 String [] foo = new String [size];
1337 foo = whereArgs.toArray(foo);
1338 int numrows = mProvider.delete(mBaseUri,
1339 MediaStore.MediaColumns._ID + " IN (" +
1340 whereClause.toString() + ")", foo);
1341 //Log.i("@@@@@@@@@", "rows deleted: " + numrows);
1342 whereClause.setLength(0);
1348 private void postscan(final String[] directories) throws RemoteException {
1350 // handle playlists last, after we know what media files are on the storage.
1351 if (mProcessPlaylists) {
1355 if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
1356 pruneDeadThumbnailFiles();
1358 // allow GC to clean up
1362 private void releaseResources() {
1363 // release the DrmManagerClient resources
1364 if (mDrmManagerClient != null) {
1365 mDrmManagerClient.close();
1366 mDrmManagerClient = null;
1370 public void scanDirectories(String[] directories) {
1372 long start = System.currentTimeMillis();
1373 prescan(null, true);
1374 long prescan = System.currentTimeMillis();
1376 if (ENABLE_BULK_INSERTS) {
1377 // create MediaInserter for bulk inserts
1378 mMediaInserter = new MediaInserter(mMediaProvider, 500);
1381 for (int i = 0; i < directories.length; i++) {
1382 processDirectory(directories[i], mClient);
1385 if (ENABLE_BULK_INSERTS) {
1386 // flush remaining inserts
1387 mMediaInserter.flushAll();
1388 mMediaInserter = null;
1391 long scan = System.currentTimeMillis();
1392 postscan(directories);
1393 long end = System.currentTimeMillis();
1396 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n");
1397 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n");
1398 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n");
1399 Log.d(TAG, " total time: " + (end - start) + "ms\n");
1401 } catch (SQLException e) {
1402 // this might happen if the SD card is removed while the media scanner is running
1403 Log.e(TAG, "SQLException in MediaScanner.scan()", e);
1404 } catch (UnsupportedOperationException e) {
1405 // this might happen if the SD card is removed while the media scanner is running
1406 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e);
1407 } catch (RemoteException e) {
1408 Log.e(TAG, "RemoteException in MediaScanner.scan()", e);
1414 // this function is used to scan a single file
1415 public Uri scanSingleFile(String path, String mimeType) {
1417 prescan(path, true);
1419 File file = new File(path);
1420 if (!file.exists()) {
1424 // lastModified is in milliseconds on Files.
1425 long lastModifiedSeconds = file.lastModified() / 1000;
1427 // always scan the file, so we can return the content://media Uri for existing files
1428 return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(),
1429 false, true, MediaScanner.isNoMediaPath(path));
1430 } catch (RemoteException e) {
1431 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1438 private static boolean isNoMediaFile(String path) {
1439 File file = new File(path);
1440 if (file.isDirectory()) return false;
1442 // special case certain file names
1443 // I use regionMatches() instead of substring() below
1444 // to avoid memory allocation
1445 int lastSlash = path.lastIndexOf('/');
1446 if (lastSlash >= 0 && lastSlash + 2 < path.length()) {
1447 // ignore those ._* files created by MacOS
1448 if (path.regionMatches(lastSlash + 1, "._", 0, 2)) {
1452 // ignore album art files created by Windows Media Player:
1453 // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg
1454 // and AlbumArt_{...}_Small.jpg
1455 if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) {
1456 if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) ||
1457 path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) {
1460 int length = path.length() - lastSlash - 1;
1461 if ((length == 17 && path.regionMatches(
1462 true, lastSlash + 1, "AlbumArtSmall", 0, 13)) ||
1464 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) {
1472 private static HashMap<String,String> mNoMediaPaths = new HashMap<String,String>();
1473 private static HashMap<String,String> mMediaPaths = new HashMap<String,String>();
1475 /* MediaProvider calls this when a .nomedia file is added or removed */
1476 public static void clearMediaPathCache(boolean clearMediaPaths, boolean clearNoMediaPaths) {
1477 synchronized (MediaScanner.class) {
1478 if (clearMediaPaths) {
1479 mMediaPaths.clear();
1481 if (clearNoMediaPaths) {
1482 mNoMediaPaths.clear();
1487 public static boolean isNoMediaPath(String path) {
1491 // return true if file or any parent directory has name starting with a dot
1492 if (path.indexOf("/.") >= 0) {
1496 int firstSlash = path.lastIndexOf('/');
1497 if (firstSlash <= 0) {
1500 String parent = path.substring(0, firstSlash);
1502 synchronized (MediaScanner.class) {
1503 if (mNoMediaPaths.containsKey(parent)) {
1505 } else if (!mMediaPaths.containsKey(parent)) {
1506 // check to see if any parent directories have a ".nomedia" file
1507 // start from 1 so we don't bother checking in the root directory
1509 while (offset >= 0) {
1510 int slashIndex = path.indexOf('/', offset);
1511 if (slashIndex > offset) {
1512 slashIndex++; // move past slash
1513 File file = new File(path.substring(0, slashIndex) + ".nomedia");
1514 if (file.exists()) {
1515 // we have a .nomedia in one of the parent directories
1516 mNoMediaPaths.put(parent, "");
1520 offset = slashIndex;
1522 mMediaPaths.put(parent, "");
1526 return isNoMediaFile(path);
1529 public void scanMtpFile(String path, int objectHandle, int format) {
1530 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1531 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1532 File file = new File(path);
1533 long lastModifiedSeconds = file.lastModified() / 1000;
1535 if (!MediaFile.isAudioFileType(fileType) && !MediaFile.isVideoFileType(fileType) &&
1536 !MediaFile.isImageFileType(fileType) && !MediaFile.isPlayListFileType(fileType) &&
1537 !MediaFile.isDrmFileType(fileType)) {
1539 // no need to use the media scanner, but we need to update last modified and file size
1540 ContentValues values = new ContentValues();
1541 values.put(Files.FileColumns.SIZE, file.length());
1542 values.put(Files.FileColumns.DATE_MODIFIED, lastModifiedSeconds);
1544 String[] whereArgs = new String[] { Integer.toString(objectHandle) };
1545 mMediaProvider.update(Files.getMtpObjectsUri(mVolumeName), values,
1546 "_id=?", whereArgs);
1547 } catch (RemoteException e) {
1548 Log.e(TAG, "RemoteException in scanMtpFile", e);
1553 mMtpObjectHandle = objectHandle;
1554 Cursor fileList = null;
1556 if (MediaFile.isPlayListFileType(fileType)) {
1557 // build file cache so we can look up tracks in the playlist
1558 prescan(null, true);
1560 FileEntry entry = makeEntryFor(path);
1561 if (entry != null) {
1562 fileList = mMediaProvider.query(mFilesUri,
1563 FILES_PRESCAN_PROJECTION, null, null, null, null);
1564 processPlayList(entry, fileList);
1567 // MTP will create a file entry for us so we don't want to do it in prescan
1568 prescan(path, false);
1570 // always scan the file, so we can return the content://media Uri for existing files
1571 mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(),
1572 (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path));
1574 } catch (RemoteException e) {
1575 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1577 mMtpObjectHandle = 0;
1578 if (fileList != null) {
1585 FileEntry makeEntryFor(String path) {
1587 String[] selectionArgs;
1591 where = Files.FileColumns.DATA + "=?";
1592 selectionArgs = new String[] { path };
1593 c = mMediaProvider.query(mFilesUriNoNotify, FILES_PRESCAN_PROJECTION,
1594 where, selectionArgs, null, null);
1595 if (c.moveToFirst()) {
1596 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
1597 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
1598 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
1599 return new FileEntry(rowId, path, lastModified, format);
1601 } catch (RemoteException e) {
1610 // returns the number of matching file/directory names, starting from the right
1611 private int matchPaths(String path1, String path2) {
1613 int end1 = path1.length();
1614 int end2 = path2.length();
1616 while (end1 > 0 && end2 > 0) {
1617 int slash1 = path1.lastIndexOf('/', end1 - 1);
1618 int slash2 = path2.lastIndexOf('/', end2 - 1);
1619 int backSlash1 = path1.lastIndexOf('\\', end1 - 1);
1620 int backSlash2 = path2.lastIndexOf('\\', end2 - 1);
1621 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1);
1622 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2);
1623 if (start1 < 0) start1 = 0; else start1++;
1624 if (start2 < 0) start2 = 0; else start2++;
1625 int length = end1 - start1;
1626 if (end2 - start2 != length) break;
1627 if (path1.regionMatches(true, start1, path2, start2, length)) {
1637 private boolean matchEntries(long rowId, String data) {
1639 int len = mPlaylistEntries.size();
1640 boolean done = true;
1641 for (int i = 0; i < len; i++) {
1642 PlaylistEntry entry = mPlaylistEntries.get(i);
1643 if (entry.bestmatchlevel == Integer.MAX_VALUE) {
1644 continue; // this entry has been matched already
1647 if (data.equalsIgnoreCase(entry.path)) {
1648 entry.bestmatchid = rowId;
1649 entry.bestmatchlevel = Integer.MAX_VALUE;
1650 continue; // no need for path matching
1653 int matchLength = matchPaths(data, entry.path);
1654 if (matchLength > entry.bestmatchlevel) {
1655 entry.bestmatchid = rowId;
1656 entry.bestmatchlevel = matchLength;
1662 private void cachePlaylistEntry(String line, String playListDirectory) {
1663 PlaylistEntry entry = new PlaylistEntry();
1664 // watch for trailing whitespace
1665 int entryLength = line.length();
1666 while (entryLength > 0 && Character.isWhitespace(line.charAt(entryLength - 1))) entryLength--;
1667 // path should be longer than 3 characters.
1668 // avoid index out of bounds errors below by returning here.
1669 if (entryLength < 3) return;
1670 if (entryLength < line.length()) line = line.substring(0, entryLength);
1672 // does entry appear to be an absolute path?
1673 // look for Unix or DOS absolute paths
1674 char ch1 = line.charAt(0);
1675 boolean fullPath = (ch1 == '/' ||
1676 (Character.isLetter(ch1) && line.charAt(1) == ':' && line.charAt(2) == '\\'));
1677 // if we have a relative path, combine entry with playListDirectory
1679 line = playListDirectory + line;
1681 //FIXME - should we look for "../" within the path?
1683 mPlaylistEntries.add(entry);
1686 private void processCachedPlaylist(Cursor fileList, ContentValues values, Uri playlistUri) {
1687 fileList.moveToPosition(-1);
1688 while (fileList.moveToNext()) {
1689 long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
1690 String data = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
1691 if (matchEntries(rowId, data)) {
1696 int len = mPlaylistEntries.size();
1698 for (int i = 0; i < len; i++) {
1699 PlaylistEntry entry = mPlaylistEntries.get(i);
1700 if (entry.bestmatchlevel > 0) {
1703 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index));
1704 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(entry.bestmatchid));
1705 mMediaProvider.insert(playlistUri, values);
1707 } catch (RemoteException e) {
1708 Log.e(TAG, "RemoteException in MediaScanner.processCachedPlaylist()", e);
1713 mPlaylistEntries.clear();
1716 private void processM3uPlayList(String path, String playListDirectory, Uri uri,
1717 ContentValues values, Cursor fileList) {
1718 BufferedReader reader = null;
1720 File f = new File(path);
1722 reader = new BufferedReader(
1723 new InputStreamReader(new FileInputStream(f)), 8192);
1724 String line = reader.readLine();
1725 mPlaylistEntries.clear();
1726 while (line != null) {
1727 // ignore comment lines, which begin with '#'
1728 if (line.length() > 0 && line.charAt(0) != '#') {
1729 cachePlaylistEntry(line, playListDirectory);
1731 line = reader.readLine();
1734 processCachedPlaylist(fileList, values, uri);
1736 } catch (IOException e) {
1737 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1742 } catch (IOException e) {
1743 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1748 private void processPlsPlayList(String path, String playListDirectory, Uri uri,
1749 ContentValues values, Cursor fileList) {
1750 BufferedReader reader = null;
1752 File f = new File(path);
1754 reader = new BufferedReader(
1755 new InputStreamReader(new FileInputStream(f)), 8192);
1756 String line = reader.readLine();
1757 mPlaylistEntries.clear();
1758 while (line != null) {
1759 // ignore comment lines, which begin with '#'
1760 if (line.startsWith("File")) {
1761 int equals = line.indexOf('=');
1763 cachePlaylistEntry(line.substring(equals + 1), playListDirectory);
1766 line = reader.readLine();
1769 processCachedPlaylist(fileList, values, uri);
1771 } catch (IOException e) {
1772 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1777 } catch (IOException e) {
1778 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1783 class WplHandler implements ElementListener {
1785 final ContentHandler handler;
1786 String playListDirectory;
1788 public WplHandler(String playListDirectory, Uri uri, Cursor fileList) {
1789 this.playListDirectory = playListDirectory;
1791 RootElement root = new RootElement("smil");
1792 Element body = root.getChild("body");
1793 Element seq = body.getChild("seq");
1794 Element media = seq.getChild("media");
1795 media.setElementListener(this);
1797 this.handler = root.getContentHandler();
1801 public void start(Attributes attributes) {
1802 String path = attributes.getValue("", "src");
1804 cachePlaylistEntry(path, playListDirectory);
1812 ContentHandler getContentHandler() {
1817 private void processWplPlayList(String path, String playListDirectory, Uri uri,
1818 ContentValues values, Cursor fileList) {
1819 FileInputStream fis = null;
1821 File f = new File(path);
1823 fis = new FileInputStream(f);
1825 mPlaylistEntries.clear();
1826 Xml.parse(fis, Xml.findEncodingByName("UTF-8"),
1827 new WplHandler(playListDirectory, uri, fileList).getContentHandler());
1829 processCachedPlaylist(fileList, values, uri);
1831 } catch (SAXException e) {
1832 e.printStackTrace();
1833 } catch (IOException e) {
1834 e.printStackTrace();
1839 } catch (IOException e) {
1840 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e);
1845 private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException {
1846 String path = entry.mPath;
1847 ContentValues values = new ContentValues();
1848 int lastSlash = path.lastIndexOf('/');
1849 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path);
1850 Uri uri, membersUri;
1851 long rowId = entry.mRowId;
1853 // make sure we have a name
1854 String name = values.getAsString(MediaStore.Audio.Playlists.NAME);
1856 name = values.getAsString(MediaStore.MediaColumns.TITLE);
1858 // extract name from file name
1859 int lastDot = path.lastIndexOf('.');
1860 name = (lastDot < 0 ? path.substring(lastSlash + 1)
1861 : path.substring(lastSlash + 1, lastDot));
1865 values.put(MediaStore.Audio.Playlists.NAME, name);
1866 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1869 values.put(MediaStore.Audio.Playlists.DATA, path);
1870 uri = mMediaProvider.insert(mPlaylistsUri, values);
1871 rowId = ContentUris.parseId(uri);
1872 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1874 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
1875 mMediaProvider.update(uri, values, null, null);
1877 // delete members of existing playlist
1878 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1879 mMediaProvider.delete(membersUri, null, null);
1882 String playListDirectory = path.substring(0, lastSlash + 1);
1883 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1884 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1886 if (fileType == MediaFile.FILE_TYPE_M3U) {
1887 processM3uPlayList(path, playListDirectory, membersUri, values, fileList);
1888 } else if (fileType == MediaFile.FILE_TYPE_PLS) {
1889 processPlsPlayList(path, playListDirectory, membersUri, values, fileList);
1890 } else if (fileType == MediaFile.FILE_TYPE_WPL) {
1891 processWplPlayList(path, playListDirectory, membersUri, values, fileList);
1895 private void processPlayLists() throws RemoteException {
1896 Iterator<FileEntry> iterator = mPlayLists.iterator();
1897 Cursor fileList = null;
1899 // use the files uri and projection because we need the format column,
1900 // but restrict the query to just audio files
1901 fileList = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION,
1902 "media_type=2", null, null, null);
1903 while (iterator.hasNext()) {
1904 FileEntry entry = iterator.next();
1905 // only process playlist files if they are new or have been modified since the last scan
1906 if (entry.mLastModifiedChanged) {
1907 processPlayList(entry, fileList);
1910 } catch (RemoteException e1) {
1912 if (fileList != null) {
1918 private native void processDirectory(String path, MediaScannerClient client);
1919 private native void processFile(String path, String mimeType, MediaScannerClient client);
1920 private native void setLocale(String locale);
1922 public native byte[] extractAlbumArt(FileDescriptor fd);
1924 private static native final void native_init();
1925 private native final void native_setup();
1926 private native final void native_finalize();
1929 public void close() {
1930 mCloseGuard.close();
1931 if (mClosed.compareAndSet(false, true)) {
1932 mMediaProvider.close();
1938 protected void finalize() throws Throwable {
1940 mCloseGuard.warnIfOpen();