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