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