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