MediaScanner.java revision 73bb511adee5444ecd041146fbbd3677fb635949
1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
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
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
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.
15 */
16
17package android.media;
18
19import android.content.ContentValues;
20import android.content.Context;
21import android.content.IContentProvider;
22import android.content.ContentUris;
23import android.database.Cursor;
24import android.database.SQLException;
25import android.graphics.BitmapFactory;
26import android.net.Uri;
27import android.os.Process;
28import android.os.RemoteException;
29import android.os.SystemProperties;
30import android.provider.MediaStore;
31import android.provider.Settings;
32import android.provider.MediaStore.Audio;
33import android.provider.MediaStore.Images;
34import android.provider.MediaStore.Video;
35import android.provider.MediaStore.Audio.Genres;
36import android.provider.MediaStore.Audio.Playlists;
37import android.sax.Element;
38import android.sax.ElementListener;
39import android.sax.RootElement;
40import android.text.TextUtils;
41import android.util.Config;
42import android.util.Log;
43import android.util.Xml;
44
45import org.xml.sax.Attributes;
46import org.xml.sax.ContentHandler;
47import org.xml.sax.SAXException;
48
49import java.io.*;
50import java.util.ArrayList;
51import java.util.HashMap;
52import java.util.HashSet;
53import java.util.Iterator;
54
55/**
56 * Internal service helper that no-one should use directly.
57 *
58 * The way the scan currently works is:
59 * - The Java MediaScannerService creates a MediaScanner (this class), and calls
60 *   MediaScanner.scanDirectories on it.
61 * - scanDirectories() calls the native processDirectory() for each of the specified directories.
62 * - the processDirectory() JNI method wraps the provided mediascanner client in a native
63 *   'MyMediaScannerClient' class, then calls processDirectory() on the native MediaScanner
64 *   object (which got created when the Java MediaScanner was created).
65 * - native MediaScanner.processDirectory() (currently part of opencore) calls
66 *   doProcessDirectory(), which recurses over the folder, and calls
67 *   native MyMediaScannerClient.scanFile() for every file whose extension matches.
68 * - native MyMediaScannerClient.scanFile() calls back on Java MediaScannerClient.scanFile,
69 *   which calls doScanFile, which after some setup calls back down to native code, calling
70 *   MediaScanner.processFile().
71 * - MediaScanner.processFile() calls one of several methods, depending on the type of the
72 *   file: parseMP3, parseMP4, parseMidi, parseOgg or parseWMA.
73 * - each of these methods gets metadata key/value pairs from the file, and repeatedly
74 *   calls native MyMediaScannerClient.handleStringTag, which calls back up to its Java
75 *   counterparts in this file.
76 * - Java handleStringTag() gathers the key/value pairs that it's interested in.
77 * - once processFile returns and we're back in Java code in doScanFile(), it calls
78 *   Java MyMediaScannerClient.endFile(), which takes all the data that's been
79 *   gathered and inserts an entry in to the database.
80 *
81 * In summary:
82 * Java MediaScannerService calls
83 * Java MediaScanner scanDirectories, which calls
84 * Java MediaScanner processDirectory (native method), which calls
85 * native MediaScanner processDirectory, which calls
86 * native MyMediaScannerClient scanFile, which calls
87 * Java MyMediaScannerClient scanFile, which calls
88 * Java MediaScannerClient doScanFile, which calls
89 * Java MediaScanner processFile (native method), which calls
90 * native MediaScanner processFile, which calls
91 * native parseMP3, parseMP4, parseMidi, parseOgg or parseWMA, which calls
92 * native MyMediaScanner handleStringTag, which calls
93 * Java MyMediaScanner handleStringTag.
94 * Once MediaScanner processFile returns, an entry is inserted in to the database.
95 *
96 * {@hide}
97 */
98public class MediaScanner
99{
100    static {
101        System.loadLibrary("media_jni");
102    }
103
104    private final static String TAG = "MediaScanner";
105
106    private static final String[] AUDIO_PROJECTION = new String[] {
107            Audio.Media._ID, // 0
108            Audio.Media.DATA, // 1
109            Audio.Media.DATE_MODIFIED, // 2
110    };
111
112    private static final int ID_AUDIO_COLUMN_INDEX = 0;
113    private static final int PATH_AUDIO_COLUMN_INDEX = 1;
114    private static final int DATE_MODIFIED_AUDIO_COLUMN_INDEX = 2;
115
116    private static final String[] VIDEO_PROJECTION = new String[] {
117            Video.Media._ID, // 0
118            Video.Media.DATA, // 1
119            Video.Media.DATE_MODIFIED, // 2
120    };
121
122    private static final int ID_VIDEO_COLUMN_INDEX = 0;
123    private static final int PATH_VIDEO_COLUMN_INDEX = 1;
124    private static final int DATE_MODIFIED_VIDEO_COLUMN_INDEX = 2;
125
126    private static final String[] IMAGES_PROJECTION = new String[] {
127            Images.Media._ID, // 0
128            Images.Media.DATA, // 1
129            Images.Media.DATE_MODIFIED, // 2
130    };
131
132    private static final int ID_IMAGES_COLUMN_INDEX = 0;
133    private static final int PATH_IMAGES_COLUMN_INDEX = 1;
134    private static final int DATE_MODIFIED_IMAGES_COLUMN_INDEX = 2;
135
136    private static final String[] PLAYLISTS_PROJECTION = new String[] {
137            Audio.Playlists._ID, // 0
138            Audio.Playlists.DATA, // 1
139            Audio.Playlists.DATE_MODIFIED, // 2
140    };
141
142    private static final String[] PLAYLIST_MEMBERS_PROJECTION = new String[] {
143            Audio.Playlists.Members.PLAYLIST_ID, // 0
144     };
145
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;
149
150    private static final String[] GENRE_LOOKUP_PROJECTION = new String[] {
151            Audio.Genres._ID, // 0
152            Audio.Genres.NAME, // 1
153    };
154
155    private static final String RINGTONES_DIR = "/ringtones/";
156    private static final String NOTIFICATIONS_DIR = "/notifications/";
157    private static final String ALARMS_DIR = "/alarms/";
158    private static final String MUSIC_DIR = "/music/";
159    private static final String PODCAST_DIR = "/podcasts/";
160
161    private static final String[] ID3_GENRES = {
162        // ID3v1 Genres
163        "Blues",
164        "Classic Rock",
165        "Country",
166        "Dance",
167        "Disco",
168        "Funk",
169        "Grunge",
170        "Hip-Hop",
171        "Jazz",
172        "Metal",
173        "New Age",
174        "Oldies",
175        "Other",
176        "Pop",
177        "R&B",
178        "Rap",
179        "Reggae",
180        "Rock",
181        "Techno",
182        "Industrial",
183        "Alternative",
184        "Ska",
185        "Death Metal",
186        "Pranks",
187        "Soundtrack",
188        "Euro-Techno",
189        "Ambient",
190        "Trip-Hop",
191        "Vocal",
192        "Jazz+Funk",
193        "Fusion",
194        "Trance",
195        "Classical",
196        "Instrumental",
197        "Acid",
198        "House",
199        "Game",
200        "Sound Clip",
201        "Gospel",
202        "Noise",
203        "AlternRock",
204        "Bass",
205        "Soul",
206        "Punk",
207        "Space",
208        "Meditative",
209        "Instrumental Pop",
210        "Instrumental Rock",
211        "Ethnic",
212        "Gothic",
213        "Darkwave",
214        "Techno-Industrial",
215        "Electronic",
216        "Pop-Folk",
217        "Eurodance",
218        "Dream",
219        "Southern Rock",
220        "Comedy",
221        "Cult",
222        "Gangsta",
223        "Top 40",
224        "Christian Rap",
225        "Pop/Funk",
226        "Jungle",
227        "Native American",
228        "Cabaret",
229        "New Wave",
230        "Psychadelic",
231        "Rave",
232        "Showtunes",
233        "Trailer",
234        "Lo-Fi",
235        "Tribal",
236        "Acid Punk",
237        "Acid Jazz",
238        "Polka",
239        "Retro",
240        "Musical",
241        "Rock & Roll",
242        "Hard Rock",
243        // The following genres are Winamp extensions
244        "Folk",
245        "Folk-Rock",
246        "National Folk",
247        "Swing",
248        "Fast Fusion",
249        "Bebob",
250        "Latin",
251        "Revival",
252        "Celtic",
253        "Bluegrass",
254        "Avantgarde",
255        "Gothic Rock",
256        "Progressive Rock",
257        "Psychedelic Rock",
258        "Symphonic Rock",
259        "Slow Rock",
260        "Big Band",
261        "Chorus",
262        "Easy Listening",
263        "Acoustic",
264        "Humour",
265        "Speech",
266        "Chanson",
267        "Opera",
268        "Chamber Music",
269        "Sonata",
270        "Symphony",
271        "Booty Bass",
272        "Primus",
273        "Porn Groove",
274        "Satire",
275        "Slow Jam",
276        "Club",
277        "Tango",
278        "Samba",
279        "Folklore",
280        "Ballad",
281        "Power Ballad",
282        "Rhythmic Soul",
283        "Freestyle",
284        "Duet",
285        "Punk Rock",
286        "Drum Solo",
287        "A capella",
288        "Euro-House",
289        "Dance Hall"
290    };
291
292    private int mNativeContext;
293    private Context mContext;
294    private IContentProvider mMediaProvider;
295    private Uri mAudioUri;
296    private Uri mVideoUri;
297    private Uri mImagesUri;
298    private Uri mThumbsUri;
299    private Uri mGenresUri;
300    private Uri mPlaylistsUri;
301    private boolean mProcessPlaylists, mProcessGenres;
302
303    // used when scanning the image database so we know whether we have to prune
304    // old thumbnail files
305    private int mOriginalCount;
306    /** Whether the scanner has set a default sound for the ringer ringtone. */
307    private boolean mDefaultRingtoneSet;
308    /** Whether the scanner has set a default sound for the notification ringtone. */
309    private boolean mDefaultNotificationSet;
310    /** The filename for the default sound for the ringer ringtone. */
311    private String mDefaultRingtoneFilename;
312    /** The filename for the default sound for the notification ringtone. */
313    private String mDefaultNotificationFilename;
314    /**
315     * The prefix for system properties that define the default sound for
316     * ringtones. Concatenate the name of the setting from Settings
317     * to get the full system property.
318     */
319    private static final String DEFAULT_RINGTONE_PROPERTY_PREFIX = "ro.config.";
320
321    // set to true if file path comparisons should be case insensitive.
322    // this should be set when scanning files on a case insensitive file system.
323    private boolean mCaseInsensitivePaths;
324
325    private BitmapFactory.Options mBitmapOptions = new BitmapFactory.Options();
326
327    private static class FileCacheEntry {
328        Uri mTableUri;
329        long mRowId;
330        String mPath;
331        long mLastModified;
332        boolean mSeenInFileSystem;
333        boolean mLastModifiedChanged;
334
335        FileCacheEntry(Uri tableUri, long rowId, String path, long lastModified) {
336            mTableUri = tableUri;
337            mRowId = rowId;
338            mPath = path;
339            mLastModified = lastModified;
340            mSeenInFileSystem = false;
341            mLastModifiedChanged = false;
342        }
343
344        @Override
345        public String toString() {
346            return mPath;
347        }
348    }
349
350    // hashes file path to FileCacheEntry.
351    // path should be lower case if mCaseInsensitivePaths is true
352    private HashMap<String, FileCacheEntry> mFileCache;
353
354    private ArrayList<FileCacheEntry> mPlayLists;
355    private HashMap<String, Uri> mGenreCache;
356
357
358    public MediaScanner(Context c) {
359        native_setup();
360        mContext = c;
361        mBitmapOptions.inSampleSize = 1;
362        mBitmapOptions.inJustDecodeBounds = true;
363
364        setDefaultRingtoneFileNames();
365    }
366
367    private void setDefaultRingtoneFileNames() {
368        mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
369                + Settings.System.RINGTONE);
370        mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
371                + Settings.System.NOTIFICATION_SOUND);
372    }
373
374    private MyMediaScannerClient mClient = new MyMediaScannerClient();
375
376    private class MyMediaScannerClient implements MediaScannerClient {
377
378        private String mArtist;
379        private String mAlbumArtist;    // use this if mArtist is missing
380        private String mAlbum;
381        private String mTitle;
382        private String mComposer;
383        private String mGenre;
384        private String mMimeType;
385        private int mFileType;
386        private int mTrack;
387        private int mYear;
388        private int mDuration;
389        private String mPath;
390        private long mLastModified;
391        private long mFileSize;
392
393        public FileCacheEntry beginFile(String path, String mimeType, long lastModified, long fileSize) {
394
395            // special case certain file names
396            // I use regionMatches() instead of substring() below
397            // to avoid memory allocation
398            int lastSlash = path.lastIndexOf('/');
399            if (lastSlash >= 0 && lastSlash + 2 < path.length()) {
400                // ignore those ._* files created by MacOS
401                if (path.regionMatches(lastSlash + 1, "._", 0, 2)) {
402                    return null;
403                }
404
405                // ignore album art files created by Windows Media Player:
406                // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg and AlbumArt_{...}_Small.jpg
407                if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) {
408                    if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) ||
409                            path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) {
410                        return null;
411                    }
412                    int length = path.length() - lastSlash - 1;
413                    if ((length == 17 && path.regionMatches(true, lastSlash + 1, "AlbumArtSmall", 0, 13)) ||
414                            (length == 10 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) {
415                        return null;
416                    }
417                }
418            }
419
420            mMimeType = null;
421            // try mimeType first, if it is specified
422            if (mimeType != null) {
423                mFileType = MediaFile.getFileTypeForMimeType(mimeType);
424                if (mFileType != 0) {
425                    mMimeType = mimeType;
426                }
427            }
428            mFileSize = fileSize;
429
430            // if mimeType was not specified, compute file type based on file extension.
431            if (mMimeType == null) {
432                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
433                if (mediaFileType != null) {
434                    mFileType = mediaFileType.fileType;
435                    mMimeType = mediaFileType.mimeType;
436                }
437            }
438
439            String key = path;
440            if (mCaseInsensitivePaths) {
441                key = path.toLowerCase();
442            }
443            FileCacheEntry entry = mFileCache.get(key);
444            if (entry == null) {
445                entry = new FileCacheEntry(null, 0, path, 0);
446                mFileCache.put(key, entry);
447            }
448            entry.mSeenInFileSystem = true;
449
450            // add some slack to avoid a rounding error
451            long delta = lastModified - entry.mLastModified;
452            if (delta > 1 || delta < -1) {
453                entry.mLastModified = lastModified;
454                entry.mLastModifiedChanged = true;
455            }
456
457            if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
458                mPlayLists.add(entry);
459                // we don't process playlists in the main scan, so return null
460                return null;
461            }
462
463            // clear all the metadata
464            mArtist = null;
465            mAlbumArtist = null;
466            mAlbum = null;
467            mTitle = null;
468            mComposer = null;
469            mGenre = null;
470            mTrack = 0;
471            mYear = 0;
472            mDuration = 0;
473            mPath = path;
474            mLastModified = lastModified;
475
476            return entry;
477        }
478
479        public void scanFile(String path, long lastModified, long fileSize) {
480            doScanFile(path, null, lastModified, fileSize, false);
481        }
482
483        public void scanFile(String path, String mimeType, long lastModified, long fileSize) {
484            doScanFile(path, mimeType, lastModified, fileSize, false);
485        }
486
487        public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean scanAlways) {
488            Uri result = null;
489//            long t1 = System.currentTimeMillis();
490            try {
491                FileCacheEntry entry = beginFile(path, mimeType, lastModified, fileSize);
492                // rescan for metadata if file was modified since last scan
493                if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
494                    String lowpath = path.toLowerCase();
495                    boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
496                    boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
497                    boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
498                    boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
499                    boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
500                        (!ringtones && !notifications && !alarms && !podcasts);
501
502                    if (mFileType == MediaFile.FILE_TYPE_MP3 ||
503                            mFileType == MediaFile.FILE_TYPE_MP4 ||
504                            mFileType == MediaFile.FILE_TYPE_M4A ||
505                            mFileType == MediaFile.FILE_TYPE_3GPP ||
506                            mFileType == MediaFile.FILE_TYPE_3GPP2 ||
507                            mFileType == MediaFile.FILE_TYPE_OGG ||
508                            mFileType == MediaFile.FILE_TYPE_MID ||
509                            mFileType == MediaFile.FILE_TYPE_WMA) {
510                        // we only extract metadata from MP3, M4A, OGG, MID and WMA files.
511                        // check MP4 files, to determine if they contain only audio.
512                        processFile(path, mimeType, this);
513                    } else if (MediaFile.isImageFileType(mFileType)) {
514                        // we used to compute the width and height but it's not worth it
515                    }
516
517                    result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
518                }
519            } catch (RemoteException e) {
520                Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
521            }
522//            long t2 = System.currentTimeMillis();
523//            Log.v(TAG, "scanFile: " + path + " took " + (t2-t1));
524            return result;
525        }
526
527        private int parseSubstring(String s, int start, int defaultValue) {
528            int length = s.length();
529            if (start == length) return defaultValue;
530
531            char ch = s.charAt(start++);
532            // return defaultValue if we have no integer at all
533            if (ch < '0' || ch > '9') return defaultValue;
534
535            int result = ch - '0';
536            while (start < length) {
537                ch = s.charAt(start++);
538                if (ch < '0' || ch > '9') return result;
539                result = result * 10 + (ch - '0');
540            }
541
542            return result;
543        }
544
545        public void handleStringTag(String name, String value) {
546            if (name.equalsIgnoreCase("title") || name.startsWith("title;")) {
547                // Don't trim() here, to preserve the special \001 character
548                // used to force sorting. The media provider will trim() before
549                // inserting the title in to the database.
550                mTitle = value;
551            } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) {
552                mArtist = value.trim();
553            } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")) {
554                mAlbumArtist = value.trim();
555            } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) {
556                mAlbum = value.trim();
557            } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) {
558                mComposer = value.trim();
559            } else if (name.equalsIgnoreCase("genre") || name.startsWith("genre;")) {
560                // handle numeric genres, which PV sometimes encodes like "(20)"
561                if (value.length() > 0) {
562                    int genreCode = -1;
563                    char ch = value.charAt(0);
564                    if (ch == '(') {
565                        genreCode = parseSubstring(value, 1, -1);
566                    } else if (ch >= '0' && ch <= '9') {
567                        genreCode = parseSubstring(value, 0, -1);
568                    }
569                    if (genreCode >= 0 && genreCode < ID3_GENRES.length) {
570                        value = ID3_GENRES[genreCode];
571                    }
572                }
573                mGenre = value;
574            } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) {
575                mYear = parseSubstring(value, 0, 0);
576            } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) {
577                // track number might be of the form "2/12"
578                // we just read the number before the slash
579                int num = parseSubstring(value, 0, 0);
580                mTrack = (mTrack / 1000) * 1000 + num;
581            } else if (name.equalsIgnoreCase("discnumber") ||
582                    name.equals("set") || name.startsWith("set;")) {
583                // set number might be of the form "1/3"
584                // we just read the number before the slash
585                int num = parseSubstring(value, 0, 0);
586                mTrack = (num * 1000) + (mTrack % 1000);
587            } else if (name.equalsIgnoreCase("duration")) {
588                mDuration = parseSubstring(value, 0, 0);
589            }
590        }
591
592        public void setMimeType(String mimeType) {
593            mMimeType = mimeType;
594            mFileType = MediaFile.getFileTypeForMimeType(mimeType);
595        }
596
597        /**
598         * Formats the data into a values array suitable for use with the Media
599         * Content Provider.
600         *
601         * @return a map of values
602         */
603        private ContentValues toValues() {
604            ContentValues map = new ContentValues();
605
606            map.put(MediaStore.MediaColumns.DATA, mPath);
607            map.put(MediaStore.MediaColumns.TITLE, mTitle);
608            map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified);
609            map.put(MediaStore.MediaColumns.SIZE, mFileSize);
610            map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType);
611
612            if (MediaFile.isVideoFileType(mFileType)) {
613                map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaFile.UNKNOWN_STRING));
614                map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaFile.UNKNOWN_STRING));
615                map.put(Video.Media.DURATION, mDuration);
616                // FIXME - add RESOLUTION
617            } else if (MediaFile.isImageFileType(mFileType)) {
618                // FIXME - add DESCRIPTION
619                // map.put(field, value);
620            } else if (MediaFile.isAudioFileType(mFileType)) {
621                map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaFile.UNKNOWN_STRING));
622                map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaFile.UNKNOWN_STRING));
623                map.put(Audio.Media.COMPOSER, mComposer);
624                if (mYear != 0) {
625                    map.put(Audio.Media.YEAR, mYear);
626                }
627                map.put(Audio.Media.TRACK, mTrack);
628                map.put(Audio.Media.DURATION, mDuration);
629            }
630            return map;
631        }
632
633        private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications,
634                boolean alarms, boolean music, boolean podcasts)
635                throws RemoteException {
636            // update database
637            Uri tableUri;
638            boolean isAudio = MediaFile.isAudioFileType(mFileType);
639            boolean isVideo = MediaFile.isVideoFileType(mFileType);
640            boolean isImage = MediaFile.isImageFileType(mFileType);
641            if (isVideo) {
642                tableUri = mVideoUri;
643            } else if (isImage) {
644                tableUri = mImagesUri;
645            } else if (isAudio) {
646                tableUri = mAudioUri;
647            } else {
648                // don't add file to database if not audio, video or image
649                return null;
650            }
651            entry.mTableUri = tableUri;
652
653             // use album artist if artist is missing
654            if (mArtist == null || mArtist.length() == 0) {
655                mArtist = mAlbumArtist;
656            }
657
658            ContentValues values = toValues();
659            String title = values.getAsString(MediaStore.MediaColumns.TITLE);
660            if (TextUtils.isEmpty(title)) {
661                title = values.getAsString(MediaStore.MediaColumns.DATA);
662                // extract file name after last slash
663                int lastSlash = title.lastIndexOf('/');
664                if (lastSlash >= 0) {
665                    lastSlash++;
666                    if (lastSlash < title.length()) {
667                        title = title.substring(lastSlash);
668                    }
669                }
670                // truncate the file extension (if any)
671                int lastDot = title.lastIndexOf('.');
672                if (lastDot > 0) {
673                    title = title.substring(0, lastDot);
674                }
675                values.put(MediaStore.MediaColumns.TITLE, title);
676            }
677            if (isAudio) {
678                values.put(Audio.Media.IS_RINGTONE, ringtones);
679                values.put(Audio.Media.IS_NOTIFICATION, notifications);
680                values.put(Audio.Media.IS_ALARM, alarms);
681                values.put(Audio.Media.IS_MUSIC, music);
682                values.put(Audio.Media.IS_PODCAST, podcasts);
683            } else if (isImage) {
684                // nothing right now
685            }
686
687            Uri result = null;
688            long rowId = entry.mRowId;
689            if (rowId == 0) {
690                // new file, insert it
691                result = mMediaProvider.insert(tableUri, values);
692                if (result != null) {
693                    rowId = ContentUris.parseId(result);
694                    entry.mRowId = rowId;
695                }
696            } else {
697                // updated file
698                result = ContentUris.withAppendedId(tableUri, rowId);
699                mMediaProvider.update(result, values, null, null);
700            }
701            if (mProcessGenres && mGenre != null) {
702                String genre = mGenre;
703                Uri uri = mGenreCache.get(genre);
704                if (uri == null) {
705                    Cursor cursor = null;
706                    try {
707                        // see if the genre already exists
708                        cursor = mMediaProvider.query(
709                                mGenresUri,
710                                GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?",
711                                        new String[] { genre }, null);
712                        if (cursor == null || cursor.getCount() == 0) {
713                            // genre does not exist, so create the genre in the genre table
714                            values.clear();
715                            values.put(MediaStore.Audio.Genres.NAME, genre);
716                            uri = mMediaProvider.insert(mGenresUri, values);
717                        } else {
718                            // genre already exists, so compute its Uri
719                            cursor.moveToNext();
720                            uri = ContentUris.withAppendedId(mGenresUri, cursor.getLong(0));
721                        }
722                        if (uri != null) {
723                            uri = Uri.withAppendedPath(uri, Genres.Members.CONTENT_DIRECTORY);
724                            mGenreCache.put(genre, uri);
725                        }
726                    } finally {
727                        // release the cursor if it exists
728                        if (cursor != null) {
729                            cursor.close();
730                        }
731                    }
732                }
733
734                if (uri != null) {
735                    // add entry to audio_genre_map
736                    values.clear();
737                    values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId));
738                    mMediaProvider.insert(uri, values);
739                }
740            }
741
742            if (notifications && !mDefaultNotificationSet) {
743                if (TextUtils.isEmpty(mDefaultNotificationFilename) ||
744                        doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) {
745                    setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
746                    mDefaultNotificationSet = true;
747                }
748            } else if (ringtones && !mDefaultRingtoneSet) {
749                if (TextUtils.isEmpty(mDefaultRingtoneFilename) ||
750                        doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) {
751                    setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
752                    mDefaultRingtoneSet = true;
753                }
754            }
755
756            return result;
757        }
758
759        private boolean doesPathHaveFilename(String path, String filename) {
760            int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1;
761            int filenameLength = filename.length();
762            return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) &&
763                    pathFilenameStart + filenameLength == path.length();
764        }
765
766        private void setSettingIfNotSet(String settingName, Uri uri, long rowId) {
767
768            String existingSettingValue = Settings.System.getString(mContext.getContentResolver(),
769                    settingName);
770
771            if (TextUtils.isEmpty(existingSettingValue)) {
772                // Set the setting to the given URI
773                Settings.System.putString(mContext.getContentResolver(), settingName,
774                        ContentUris.withAppendedId(uri, rowId).toString());
775            }
776        }
777
778    }; // end of anonymous MediaScannerClient instance
779
780    private void prescan(String filePath) throws RemoteException {
781        Cursor c = null;
782        String where = null;
783        String[] selectionArgs = null;
784
785        if (mFileCache == null) {
786            mFileCache = new HashMap<String, FileCacheEntry>();
787        } else {
788            mFileCache.clear();
789        }
790        if (mPlayLists == null) {
791            mPlayLists = new ArrayList<FileCacheEntry>();
792        } else {
793            mPlayLists.clear();
794        }
795
796        // Build the list of files from the content provider
797        try {
798            // Read existing files from the audio table
799            if (filePath != null) {
800                where = MediaStore.Audio.Media.DATA + "=?";
801                selectionArgs = new String[] { filePath };
802            }
803            c = mMediaProvider.query(mAudioUri, AUDIO_PROJECTION, where, selectionArgs, null);
804
805            if (c != null) {
806                try {
807                    while (c.moveToNext()) {
808                        long rowId = c.getLong(ID_AUDIO_COLUMN_INDEX);
809                        String path = c.getString(PATH_AUDIO_COLUMN_INDEX);
810                        long lastModified = c.getLong(DATE_MODIFIED_AUDIO_COLUMN_INDEX);
811
812                        String key = path;
813                        if (mCaseInsensitivePaths) {
814                            key = path.toLowerCase();
815                        }
816                        mFileCache.put(key, new FileCacheEntry(mAudioUri, rowId, path,
817                                lastModified));
818                    }
819                } finally {
820                    c.close();
821                    c = null;
822                }
823            }
824
825            // Read existing files from the video table
826            if (filePath != null) {
827                where = MediaStore.Video.Media.DATA + "=?";
828            } else {
829                where = null;
830            }
831            c = mMediaProvider.query(mVideoUri, VIDEO_PROJECTION, where, selectionArgs, null);
832
833            if (c != null) {
834                try {
835                    while (c.moveToNext()) {
836                        long rowId = c.getLong(ID_VIDEO_COLUMN_INDEX);
837                        String path = c.getString(PATH_VIDEO_COLUMN_INDEX);
838                        long lastModified = c.getLong(DATE_MODIFIED_VIDEO_COLUMN_INDEX);
839
840                        String key = path;
841                        if (mCaseInsensitivePaths) {
842                            key = path.toLowerCase();
843                        }
844                        mFileCache.put(key, new FileCacheEntry(mVideoUri, rowId, path,
845                                lastModified));
846                    }
847                } finally {
848                    c.close();
849                    c = null;
850                }
851            }
852
853            // Read existing files from the images table
854            if (filePath != null) {
855                where = MediaStore.Images.Media.DATA + "=?";
856            } else {
857                where = null;
858            }
859            mOriginalCount = 0;
860            c = mMediaProvider.query(mImagesUri, IMAGES_PROJECTION, where, selectionArgs, null);
861
862            if (c != null) {
863                try {
864                    mOriginalCount = c.getCount();
865                    while (c.moveToNext()) {
866                        long rowId = c.getLong(ID_IMAGES_COLUMN_INDEX);
867                        String path = c.getString(PATH_IMAGES_COLUMN_INDEX);
868                       long lastModified = c.getLong(DATE_MODIFIED_IMAGES_COLUMN_INDEX);
869
870                        String key = path;
871                        if (mCaseInsensitivePaths) {
872                            key = path.toLowerCase();
873                        }
874                        mFileCache.put(key, new FileCacheEntry(mImagesUri, rowId, path,
875                                lastModified));
876                    }
877                } finally {
878                    c.close();
879                    c = null;
880                }
881            }
882
883            if (mProcessPlaylists) {
884                // Read existing files from the playlists table
885                if (filePath != null) {
886                    where = MediaStore.Audio.Playlists.DATA + "=?";
887                } else {
888                    where = null;
889                }
890                c = mMediaProvider.query(mPlaylistsUri, PLAYLISTS_PROJECTION, where, selectionArgs, null);
891
892                if (c != null) {
893                    try {
894                        while (c.moveToNext()) {
895                            String path = c.getString(PATH_IMAGES_COLUMN_INDEX);
896
897                            if (path != null && path.length() > 0) {
898                                long rowId = c.getLong(ID_PLAYLISTS_COLUMN_INDEX);
899                                long lastModified = c.getLong(DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX);
900
901                                String key = path;
902                                if (mCaseInsensitivePaths) {
903                                    key = path.toLowerCase();
904                                }
905                                mFileCache.put(key, new FileCacheEntry(mPlaylistsUri, rowId, path,
906                                        lastModified));
907                            }
908                        }
909                    } finally {
910                        c.close();
911                        c = null;
912                    }
913                }
914            }
915        }
916        finally {
917            if (c != null) {
918                c.close();
919            }
920        }
921    }
922
923    private boolean inScanDirectory(String path, String[] directories) {
924        for (int i = 0; i < directories.length; i++) {
925            if (path.startsWith(directories[i])) {
926                return true;
927            }
928        }
929        return false;
930    }
931
932    private void pruneDeadThumbnailFiles() {
933        HashSet<String> existingFiles = new HashSet<String>();
934        String directory = "/sdcard/DCIM/.thumbnails";
935        String [] files = (new File(directory)).list();
936        if (files == null)
937            files = new String[0];
938
939        for (int i = 0; i < files.length; i++) {
940            String fullPathString = directory + "/" + files[i];
941            existingFiles.add(fullPathString);
942        }
943
944        try {
945            Cursor c = mMediaProvider.query(
946                    mThumbsUri,
947                    new String [] { "_data" },
948                    null,
949                    null,
950                    null);
951            Log.v(TAG, "pruneDeadThumbnailFiles... " + c);
952            if (c != null && c.moveToFirst()) {
953                do {
954                    String fullPathString = c.getString(0);
955                    existingFiles.remove(fullPathString);
956                } while (c.moveToNext());
957            }
958
959            for (String fileToDelete : existingFiles) {
960                if (Config.LOGV)
961                    Log.v(TAG, "fileToDelete is " + fileToDelete);
962                try {
963                    (new File(fileToDelete)).delete();
964                } catch (SecurityException ex) {
965                }
966            }
967
968            Log.v(TAG, "/pruneDeadThumbnailFiles... " + c);
969            if (c != null) {
970                c.close();
971            }
972        } catch (RemoteException e) {
973            // We will soon be killed...
974        }
975    }
976
977    private void postscan(String[] directories) throws RemoteException {
978        Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();
979
980        while (iterator.hasNext()) {
981            FileCacheEntry entry = iterator.next();
982            String path = entry.mPath;
983
984            // remove database entries for files that no longer exist.
985            boolean fileMissing = false;
986
987            if (!entry.mSeenInFileSystem) {
988                if (inScanDirectory(path, directories)) {
989                    // we didn't see this file in the scan directory.
990                    fileMissing = true;
991                } else {
992                    // the file is outside of our scan directory,
993                    // so we need to check for file existence here.
994                    File testFile = new File(path);
995                    if (!testFile.exists()) {
996                        fileMissing = true;
997                    }
998                }
999            }
1000
1001            if (fileMissing) {
1002                // do not delete missing playlists, since they may have been modified by the user.
1003                // the user can delete them in the media player instead.
1004                // instead, clear the path and lastModified fields in the row
1005                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1006                int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1007
1008                if (MediaFile.isPlayListFileType(fileType)) {
1009                    ContentValues values = new ContentValues();
1010                    values.put(MediaStore.Audio.Playlists.DATA, "");
1011                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, 0);
1012                    mMediaProvider.update(ContentUris.withAppendedId(mPlaylistsUri, entry.mRowId), values, null, null);
1013                } else {
1014                    mMediaProvider.delete(ContentUris.withAppendedId(entry.mTableUri, entry.mRowId), null, null);
1015                    iterator.remove();
1016                }
1017            }
1018        }
1019
1020        // handle playlists last, after we know what media files are on the storage.
1021        if (mProcessPlaylists) {
1022            processPlayLists();
1023        }
1024
1025        if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
1026            pruneDeadThumbnailFiles();
1027
1028        // allow GC to clean up
1029        mGenreCache = null;
1030        mPlayLists = null;
1031        mFileCache = null;
1032        mMediaProvider = null;
1033    }
1034
1035    private void initialize(String volumeName) {
1036        mMediaProvider = mContext.getContentResolver().acquireProvider("media");
1037
1038        mAudioUri = Audio.Media.getContentUri(volumeName);
1039        mVideoUri = Video.Media.getContentUri(volumeName);
1040        mImagesUri = Images.Media.getContentUri(volumeName);
1041        mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
1042
1043        if (!volumeName.equals("internal")) {
1044            // we only support playlists on external media
1045            mProcessPlaylists = true;
1046            mProcessGenres = true;
1047            mGenreCache = new HashMap<String, Uri>();
1048            mGenresUri = Genres.getContentUri(volumeName);
1049            mPlaylistsUri = Playlists.getContentUri(volumeName);
1050            // assuming external storage is FAT (case insensitive), except on the simulator.
1051            if ( Process.supportsProcesses()) {
1052                mCaseInsensitivePaths = true;
1053            }
1054        }
1055    }
1056
1057    public void scanDirectories(String[] directories, String volumeName) {
1058        try {
1059            long start = System.currentTimeMillis();
1060            initialize(volumeName);
1061            prescan(null);
1062            long prescan = System.currentTimeMillis();
1063
1064            for (int i = 0; i < directories.length; i++) {
1065                processDirectory(directories[i], MediaFile.sFileExtensions, mClient);
1066            }
1067            long scan = System.currentTimeMillis();
1068            postscan(directories);
1069            long end = System.currentTimeMillis();
1070
1071            if (Config.LOGD) {
1072                Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n");
1073                Log.d(TAG, "    scan time: " + (scan - prescan) + "ms\n");
1074                Log.d(TAG, "postscan time: " + (end - scan) + "ms\n");
1075                Log.d(TAG, "   total time: " + (end - start) + "ms\n");
1076            }
1077        } catch (SQLException e) {
1078            // this might happen if the SD card is removed while the media scanner is running
1079            Log.e(TAG, "SQLException in MediaScanner.scan()", e);
1080        } catch (UnsupportedOperationException e) {
1081            // this might happen if the SD card is removed while the media scanner is running
1082            Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e);
1083        } catch (RemoteException e) {
1084            Log.e(TAG, "RemoteException in MediaScanner.scan()", e);
1085        }
1086    }
1087
1088    // this function is used to scan a single file
1089    public Uri scanSingleFile(String path, String volumeName, String mimeType) {
1090        try {
1091            initialize(volumeName);
1092            prescan(path);
1093
1094            File file = new File(path);
1095            // always scan the file, so we can return the content://media Uri for existing files
1096            return mClient.doScanFile(path, mimeType, file.lastModified(), file.length(), true);
1097        } catch (RemoteException e) {
1098            Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1099            return null;
1100        }
1101    }
1102
1103    // returns the number of matching file/directory names, starting from the right
1104    private int matchPaths(String path1, String path2) {
1105        int result = 0;
1106        int end1 = path1.length();
1107        int end2 = path2.length();
1108
1109        while (end1 > 0 && end2 > 0) {
1110            int slash1 = path1.lastIndexOf('/', end1 - 1);
1111            int slash2 = path2.lastIndexOf('/', end2 - 1);
1112            int backSlash1 = path1.lastIndexOf('\\', end1 - 1);
1113            int backSlash2 = path2.lastIndexOf('\\', end2 - 1);
1114            int start1 = (slash1 > backSlash1 ? slash1 : backSlash1);
1115            int start2 = (slash2 > backSlash2 ? slash2 : backSlash2);
1116            if (start1 < 0) start1 = 0; else start1++;
1117            if (start2 < 0) start2 = 0; else start2++;
1118            int length = end1 - start1;
1119            if (end2 - start2 != length) break;
1120            if (path1.regionMatches(true, start1, path2, start2, length)) {
1121                result++;
1122                end1 = start1 - 1;
1123                end2 = start2 - 1;
1124            } else break;
1125        }
1126
1127        return result;
1128    }
1129
1130    private boolean addPlayListEntry(String entry, String playListDirectory,
1131            Uri uri, ContentValues values, int index) {
1132
1133        // watch for trailing whitespace
1134        int entryLength = entry.length();
1135        while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--;
1136        // path should be longer than 3 characters.
1137        // avoid index out of bounds errors below by returning here.
1138        if (entryLength < 3) return false;
1139        if (entryLength < entry.length()) entry = entry.substring(0, entryLength);
1140
1141        // does entry appear to be an absolute path?
1142        // look for Unix or DOS absolute paths
1143        char ch1 = entry.charAt(0);
1144        boolean fullPath = (ch1 == '/' ||
1145                (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\'));
1146        // if we have a relative path, combine entry with playListDirectory
1147        if (!fullPath)
1148            entry = playListDirectory + entry;
1149
1150        //FIXME - should we look for "../" within the path?
1151
1152        // best matching MediaFile for the play list entry
1153        FileCacheEntry bestMatch = null;
1154
1155        // number of rightmost file/directory names for bestMatch
1156        int bestMatchLength = 0;
1157
1158        Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();
1159        while (iterator.hasNext()) {
1160            FileCacheEntry cacheEntry = iterator.next();
1161            String path = cacheEntry.mPath;
1162
1163            if (path.equalsIgnoreCase(entry)) {
1164                bestMatch = cacheEntry;
1165                break;    // don't bother continuing search
1166            }
1167
1168            int matchLength = matchPaths(path, entry);
1169            if (matchLength > bestMatchLength) {
1170                bestMatch = cacheEntry;
1171                bestMatchLength = matchLength;
1172            }
1173        }
1174
1175        if (bestMatch == null) {
1176            return false;
1177        }
1178
1179        try {
1180        // OK, now we need to add this to the database
1181            values.clear();
1182            values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index));
1183            values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId));
1184            mMediaProvider.insert(uri, values);
1185        } catch (RemoteException e) {
1186            Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e);
1187            return false;
1188        }
1189
1190        return true;
1191    }
1192
1193    private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) {
1194        BufferedReader reader = null;
1195        try {
1196            File f = new File(path);
1197            if (f.exists()) {
1198                reader = new BufferedReader(
1199                        new InputStreamReader(new FileInputStream(f)), 8192);
1200                String line = reader.readLine();
1201                int index = 0;
1202                while (line != null) {
1203                    // ignore comment lines, which begin with '#'
1204                    if (line.length() > 0 && line.charAt(0) != '#') {
1205                        values.clear();
1206                        if (addPlayListEntry(line, playListDirectory, uri, values, index))
1207                            index++;
1208                    }
1209                    line = reader.readLine();
1210                }
1211            }
1212        } catch (IOException e) {
1213            Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1214        } finally {
1215            try {
1216                if (reader != null)
1217                    reader.close();
1218            } catch (IOException e) {
1219                Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1220            }
1221        }
1222    }
1223
1224    private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) {
1225        BufferedReader reader = null;
1226        try {
1227            File f = new File(path);
1228            if (f.exists()) {
1229                reader = new BufferedReader(
1230                        new InputStreamReader(new FileInputStream(f)), 8192);
1231                String line = reader.readLine();
1232                int index = 0;
1233                while (line != null) {
1234                    // ignore comment lines, which begin with '#'
1235                    if (line.startsWith("File")) {
1236                        int equals = line.indexOf('=');
1237                        if (equals > 0) {
1238                            values.clear();
1239                            if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index))
1240                                index++;
1241                        }
1242                    }
1243                    line = reader.readLine();
1244                }
1245            }
1246        } catch (IOException e) {
1247            Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1248        } finally {
1249            try {
1250                if (reader != null)
1251                    reader.close();
1252            } catch (IOException e) {
1253                Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1254            }
1255        }
1256    }
1257
1258    class WplHandler implements ElementListener {
1259
1260        final ContentHandler handler;
1261        String playListDirectory;
1262        Uri uri;
1263        ContentValues values = new ContentValues();
1264        int index = 0;
1265
1266        public WplHandler(String playListDirectory, Uri uri) {
1267            this.playListDirectory = playListDirectory;
1268            this.uri = uri;
1269
1270            RootElement root = new RootElement("smil");
1271            Element body = root.getChild("body");
1272            Element seq = body.getChild("seq");
1273            Element media = seq.getChild("media");
1274            media.setElementListener(this);
1275
1276            this.handler = root.getContentHandler();
1277        }
1278
1279        public void start(Attributes attributes) {
1280            String path = attributes.getValue("", "src");
1281            if (path != null) {
1282                values.clear();
1283                if (addPlayListEntry(path, playListDirectory, uri, values, index)) {
1284                    index++;
1285                }
1286            }
1287        }
1288
1289       public void end() {
1290       }
1291
1292        ContentHandler getContentHandler() {
1293            return handler;
1294        }
1295    }
1296
1297    private void processWplPlayList(String path, String playListDirectory, Uri uri) {
1298        FileInputStream fis = null;
1299        try {
1300            File f = new File(path);
1301            if (f.exists()) {
1302                fis = new FileInputStream(f);
1303
1304                Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler());
1305            }
1306        } catch (SAXException e) {
1307            e.printStackTrace();
1308        } catch (IOException e) {
1309            e.printStackTrace();
1310        } finally {
1311            try {
1312                if (fis != null)
1313                    fis.close();
1314            } catch (IOException e) {
1315                Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e);
1316            }
1317        }
1318    }
1319
1320    private void processPlayLists() throws RemoteException {
1321        Iterator<FileCacheEntry> iterator = mPlayLists.iterator();
1322        while (iterator.hasNext()) {
1323            FileCacheEntry entry = iterator.next();
1324            String path = entry.mPath;
1325
1326            // only process playlist files if they are new or have been modified since the last scan
1327            if (entry.mLastModifiedChanged) {
1328                ContentValues values = new ContentValues();
1329                int lastSlash = path.lastIndexOf('/');
1330                if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path);
1331                Uri uri, membersUri;
1332                long rowId = entry.mRowId;
1333                if (rowId == 0) {
1334                    // Create a new playlist
1335
1336                    int lastDot = path.lastIndexOf('.');
1337                    String name = (lastDot < 0 ? path.substring(lastSlash + 1) : path.substring(lastSlash + 1, lastDot));
1338                    values.put(MediaStore.Audio.Playlists.NAME, name);
1339                    values.put(MediaStore.Audio.Playlists.DATA, path);
1340                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1341                    uri = mMediaProvider.insert(mPlaylistsUri, values);
1342                    rowId = ContentUris.parseId(uri);
1343                    membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1344                } else {
1345                    uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
1346
1347                    // update lastModified value of existing playlist
1348                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1349                    mMediaProvider.update(uri, values, null, null);
1350
1351                    // delete members of existing playlist
1352                    membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1353                    mMediaProvider.delete(membersUri, null, null);
1354                }
1355
1356                String playListDirectory = path.substring(0, lastSlash + 1);
1357                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1358                int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1359
1360                if (fileType == MediaFile.FILE_TYPE_M3U)
1361                    processM3uPlayList(path, playListDirectory, membersUri, values);
1362                else if (fileType == MediaFile.FILE_TYPE_PLS)
1363                    processPlsPlayList(path, playListDirectory, membersUri, values);
1364                else if (fileType == MediaFile.FILE_TYPE_WPL)
1365                    processWplPlayList(path, playListDirectory, membersUri);
1366
1367                Cursor cursor = mMediaProvider.query(membersUri, PLAYLIST_MEMBERS_PROJECTION, null,
1368                        null, null);
1369                try {
1370                    if (cursor == null || cursor.getCount() == 0) {
1371                        Log.d(TAG, "playlist is empty - deleting");
1372                        mMediaProvider.delete(uri, null, null);
1373                    }
1374                } finally {
1375                    if (cursor != null) cursor.close();
1376                }
1377            }
1378        }
1379    }
1380
1381    private native void processDirectory(String path, String extensions, MediaScannerClient client);
1382    private native void processFile(String path, String mimeType, MediaScannerClient client);
1383    public native void setLocale(String locale);
1384
1385    public native byte[] extractAlbumArt(FileDescriptor fd);
1386
1387    private native final void native_setup();
1388    private native final void native_finalize();
1389    @Override
1390    protected void finalize() {
1391        mContext.getContentResolver().releaseProvider(mMediaProvider);
1392        native_finalize();
1393    }
1394}
1395