MediaScanner.java revision 10e89712863f5b91a2982dc1783fbdfe39c1485d
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        private boolean mIsDrm;
426        private boolean mNoMedia;   // flag to suppress file from appearing in media tables
427
428        public FileCacheEntry beginFile(String path, String mimeType, long lastModified,
429                long fileSize, boolean isDirectory, boolean noMedia) {
430            mMimeType = mimeType;
431            mFileType = 0;
432            mFileSize = fileSize;
433
434            if (!isDirectory) {
435                if (!noMedia && isNoMediaFile(path)) {
436                    noMedia = true;
437                }
438                mNoMedia = noMedia;
439
440                // try mimeType first, if it is specified
441                if (mimeType != null) {
442                    mFileType = MediaFile.getFileTypeForMimeType(mimeType);
443                }
444
445                // if mimeType was not specified, compute file type based on file extension.
446                if (mFileType == 0) {
447                    MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
448                    if (mediaFileType != null) {
449                        mFileType = mediaFileType.fileType;
450                        if (mMimeType == null) {
451                            mMimeType = mediaFileType.mimeType;
452                        }
453                    }
454                }
455
456                if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) {
457                    mFileType = getFileTypeFromDrm(path);
458                }
459            }
460
461            String key = path;
462            if (mCaseInsensitivePaths) {
463                key = path.toLowerCase();
464            }
465            FileCacheEntry entry = mFileCache.get(key);
466            // add some slack to avoid a rounding error
467            long delta = (entry != null) ? (lastModified - entry.mLastModified) : 0;
468            boolean wasModified = delta > 1 || delta < -1;
469            if (entry == null || wasModified) {
470                if (wasModified) {
471                    entry.mLastModified = lastModified;
472                } else {
473                    entry = new FileCacheEntry(0, path, lastModified,
474                            (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
475                    mFileCache.put(key, entry);
476                }
477                entry.mLastModifiedChanged = true;
478            }
479            entry.mSeenInFileSystem = true;
480
481            if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
482                mPlayLists.add(entry);
483                // we don't process playlists in the main scan, so return null
484                return null;
485            }
486
487            // clear all the metadata
488            mArtist = null;
489            mAlbumArtist = null;
490            mAlbum = null;
491            mTitle = null;
492            mComposer = null;
493            mGenre = null;
494            mTrack = 0;
495            mYear = 0;
496            mDuration = 0;
497            mPath = path;
498            mLastModified = lastModified;
499            mWriter = null;
500            mCompilation = 0;
501            mIsDrm = false;
502
503            return entry;
504        }
505
506        public void scanFile(String path, long lastModified, long fileSize,
507                boolean isDirectory, boolean noMedia) {
508            // This is the callback funtion from native codes.
509            // Log.v(TAG, "scanFile: "+path);
510            doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
511        }
512
513        public Uri doScanFile(String path, String mimeType, long lastModified,
514                long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
515            Uri result = null;
516//            long t1 = System.currentTimeMillis();
517            try {
518                FileCacheEntry entry = beginFile(path, mimeType, lastModified,
519                        fileSize, isDirectory, noMedia);
520                // rescan for metadata if file was modified since last scan
521                if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
522                    if (noMedia) {
523                        result = endFile(entry, false, false, false, false, false);
524                    } else {
525                        String lowpath = path.toLowerCase();
526                        boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
527                        boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
528                        boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
529                        boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
530                        boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
531                            (!ringtones && !notifications && !alarms && !podcasts);
532
533                        // we only extract metadata for audio and video files
534                        if (MediaFile.isAudioFileType(mFileType)
535                                || MediaFile.isVideoFileType(mFileType)) {
536                            processFile(path, mimeType, this);
537                        }
538
539                        result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
540                    }
541                }
542            } catch (RemoteException e) {
543                Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
544            }
545//            long t2 = System.currentTimeMillis();
546//            Log.v(TAG, "scanFile: " + path + " took " + (t2-t1));
547            return result;
548        }
549
550        private int parseSubstring(String s, int start, int defaultValue) {
551            int length = s.length();
552            if (start == length) return defaultValue;
553
554            char ch = s.charAt(start++);
555            // return defaultValue if we have no integer at all
556            if (ch < '0' || ch > '9') return defaultValue;
557
558            int result = ch - '0';
559            while (start < length) {
560                ch = s.charAt(start++);
561                if (ch < '0' || ch > '9') return result;
562                result = result * 10 + (ch - '0');
563            }
564
565            return result;
566        }
567
568        public void handleStringTag(String name, String value) {
569            if (name.equalsIgnoreCase("title") || name.startsWith("title;")) {
570                // Don't trim() here, to preserve the special \001 character
571                // used to force sorting. The media provider will trim() before
572                // inserting the title in to the database.
573                mTitle = value;
574            } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) {
575                mArtist = value.trim();
576            } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")
577                    || name.equalsIgnoreCase("band") || name.startsWith("band;")) {
578                mAlbumArtist = value.trim();
579            } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) {
580                mAlbum = value.trim();
581            } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) {
582                mComposer = value.trim();
583            } else if (name.equalsIgnoreCase("genre") || name.startsWith("genre;")) {
584                mGenre = getGenreName(value);
585            } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) {
586                mYear = parseSubstring(value, 0, 0);
587            } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) {
588                // track number might be of the form "2/12"
589                // we just read the number before the slash
590                int num = parseSubstring(value, 0, 0);
591                mTrack = (mTrack / 1000) * 1000 + num;
592            } else if (name.equalsIgnoreCase("discnumber") ||
593                    name.equals("set") || name.startsWith("set;")) {
594                // set number might be of the form "1/3"
595                // we just read the number before the slash
596                int num = parseSubstring(value, 0, 0);
597                mTrack = (num * 1000) + (mTrack % 1000);
598            } else if (name.equalsIgnoreCase("duration")) {
599                mDuration = parseSubstring(value, 0, 0);
600            } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {
601                mWriter = value.trim();
602            } else if (name.equalsIgnoreCase("compilation")) {
603                mCompilation = parseSubstring(value, 0, 0);
604            } else if (name.equalsIgnoreCase("isdrm")) {
605                mIsDrm = (parseSubstring(value, 0, 0) == 1);
606            }
607        }
608
609        public String getGenreName(String genreTagValue) {
610
611            if (genreTagValue == null) {
612                return null;
613            }
614            final int length = genreTagValue.length();
615
616            if (length > 0 && genreTagValue.charAt(0) == '(') {
617                StringBuffer number = new StringBuffer();
618                int i = 1;
619                for (; i < length - 1; ++i) {
620                    char c = genreTagValue.charAt(i);
621                    if (Character.isDigit(c)) {
622                        number.append(c);
623                    } else {
624                        break;
625                    }
626                }
627                if (genreTagValue.charAt(i) == ')') {
628                    try {
629                        short genreIndex = Short.parseShort(number.toString());
630                        if (genreIndex >= 0) {
631                            if (genreIndex < ID3_GENRES.length) {
632                                return ID3_GENRES[genreIndex];
633                            } else if (genreIndex == 0xFF) {
634                                return null;
635                            } else if (genreIndex < 0xFF && (i + 1) < length) {
636                                // genre is valid but unknown,
637                                // if there is a string after the value we take it
638                                return genreTagValue.substring(i + 1);
639                            } else {
640                                // else return the number, without parentheses
641                                return number.toString();
642                            }
643                        }
644                    } catch (NumberFormatException e) {
645                    }
646                }
647            }
648
649            return genreTagValue;
650        }
651
652        public void setMimeType(String mimeType) {
653            if ("audio/mp4".equals(mMimeType) &&
654                    mimeType.startsWith("video")) {
655                // for feature parity with Donut, we force m4a files to keep the
656                // audio/mp4 mimetype, even if they are really "enhanced podcasts"
657                // with a video track
658                return;
659            }
660            mMimeType = mimeType;
661            mFileType = MediaFile.getFileTypeForMimeType(mimeType);
662        }
663
664        /**
665         * Formats the data into a values array suitable for use with the Media
666         * Content Provider.
667         *
668         * @return a map of values
669         */
670        private ContentValues toValues() {
671            ContentValues map = new ContentValues();
672
673            map.put(MediaStore.MediaColumns.DATA, mPath);
674            map.put(MediaStore.MediaColumns.TITLE, mTitle);
675            map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified);
676            map.put(MediaStore.MediaColumns.SIZE, mFileSize);
677            map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType);
678            map.put(MediaStore.MediaColumns.IS_DRM, mIsDrm);
679
680            if (!mNoMedia) {
681                if (MediaFile.isVideoFileType(mFileType)) {
682                    map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0
683                            ? mArtist : MediaStore.UNKNOWN_STRING));
684                    map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0
685                            ? mAlbum : MediaStore.UNKNOWN_STRING));
686                    map.put(Video.Media.DURATION, mDuration);
687                    // FIXME - add RESOLUTION
688                } else if (MediaFile.isImageFileType(mFileType)) {
689                    // FIXME - add DESCRIPTION
690                } else if (MediaFile.isAudioFileType(mFileType)) {
691                    map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ?
692                            mArtist : MediaStore.UNKNOWN_STRING);
693                    map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null &&
694                            mAlbumArtist.length() > 0) ? mAlbumArtist : null);
695                    map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ?
696                            mAlbum : MediaStore.UNKNOWN_STRING);
697                    map.put(Audio.Media.COMPOSER, mComposer);
698                    if (mYear != 0) {
699                        map.put(Audio.Media.YEAR, mYear);
700                    }
701                    map.put(Audio.Media.TRACK, mTrack);
702                    map.put(Audio.Media.DURATION, mDuration);
703                    map.put(Audio.Media.COMPILATION, mCompilation);
704                }
705            }
706            return map;
707        }
708
709        private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications,
710                boolean alarms, boolean music, boolean podcasts)
711                throws RemoteException {
712            // update database
713
714            // use album artist if artist is missing
715            if (mArtist == null || mArtist.length() == 0) {
716                mArtist = mAlbumArtist;
717            }
718
719            ContentValues values = toValues();
720            String title = values.getAsString(MediaStore.MediaColumns.TITLE);
721            if (title == null || TextUtils.isEmpty(title.trim())) {
722                title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA));
723                values.put(MediaStore.MediaColumns.TITLE, title);
724            }
725            String album = values.getAsString(Audio.Media.ALBUM);
726            if (MediaStore.UNKNOWN_STRING.equals(album)) {
727                album = values.getAsString(MediaStore.MediaColumns.DATA);
728                // extract last path segment before file name
729                int lastSlash = album.lastIndexOf('/');
730                if (lastSlash >= 0) {
731                    int previousSlash = 0;
732                    while (true) {
733                        int idx = album.indexOf('/', previousSlash + 1);
734                        if (idx < 0 || idx >= lastSlash) {
735                            break;
736                        }
737                        previousSlash = idx;
738                    }
739                    if (previousSlash != 0) {
740                        album = album.substring(previousSlash + 1, lastSlash);
741                        values.put(Audio.Media.ALBUM, album);
742                    }
743                }
744            }
745            long rowId = entry.mRowId;
746            if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) {
747                // Only set these for new entries. For existing entries, they
748                // may have been modified later, and we want to keep the current
749                // values so that custom ringtones still show up in the ringtone
750                // picker.
751                values.put(Audio.Media.IS_RINGTONE, ringtones);
752                values.put(Audio.Media.IS_NOTIFICATION, notifications);
753                values.put(Audio.Media.IS_ALARM, alarms);
754                values.put(Audio.Media.IS_MUSIC, music);
755                values.put(Audio.Media.IS_PODCAST, podcasts);
756            } else if (mFileType == MediaFile.FILE_TYPE_JPEG && !mNoMedia) {
757                ExifInterface exif = null;
758                try {
759                    exif = new ExifInterface(entry.mPath);
760                } catch (IOException ex) {
761                    // exif is null
762                }
763                if (exif != null) {
764                    float[] latlng = new float[2];
765                    if (exif.getLatLong(latlng)) {
766                        values.put(Images.Media.LATITUDE, latlng[0]);
767                        values.put(Images.Media.LONGITUDE, latlng[1]);
768                    }
769
770                    long time = exif.getGpsDateTime();
771                    if (time != -1) {
772                        values.put(Images.Media.DATE_TAKEN, time);
773                    } else {
774                        // If no time zone information is available, we should consider using
775                        // EXIF local time as taken time if the difference between file time
776                        // and EXIF local time is not less than 1 Day, otherwise MediaProvider
777                        // will use file time as taken time.
778                        time = exif.getDateTime();
779                        if (Math.abs(mLastModified * 1000 - time) >= 86400000) {
780                            values.put(Images.Media.DATE_TAKEN, time);
781                        }
782                    }
783
784                    int orientation = exif.getAttributeInt(
785                        ExifInterface.TAG_ORIENTATION, -1);
786                    if (orientation != -1) {
787                        // We only recognize a subset of orientation tag values.
788                        int degree;
789                        switch(orientation) {
790                            case ExifInterface.ORIENTATION_ROTATE_90:
791                                degree = 90;
792                                break;
793                            case ExifInterface.ORIENTATION_ROTATE_180:
794                                degree = 180;
795                                break;
796                            case ExifInterface.ORIENTATION_ROTATE_270:
797                                degree = 270;
798                                break;
799                            default:
800                                degree = 0;
801                                break;
802                        }
803                        values.put(Images.Media.ORIENTATION, degree);
804                    }
805                }
806            }
807
808            Uri tableUri = mFilesUri;
809            if (!mNoMedia) {
810                if (MediaFile.isVideoFileType(mFileType)) {
811                    tableUri = mVideoUri;
812                } else if (MediaFile.isImageFileType(mFileType)) {
813                    tableUri = mImagesUri;
814                } else if (MediaFile.isAudioFileType(mFileType)) {
815                    tableUri = mAudioUri;
816                }
817            }
818            Uri result = null;
819            if (rowId == 0) {
820                if (mMtpObjectHandle != 0) {
821                    values.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle);
822                }
823                if (tableUri == mFilesUri) {
824                    int format = entry.mFormat;
825                    if (format == 0) {
826                        format = MediaFile.getFormatCode(entry.mPath, mMimeType);
827                    }
828                    values.put(Files.FileColumns.FORMAT, format);
829                }
830                // new file, insert it
831                result = mMediaProvider.insert(tableUri, values);
832                if (result != null) {
833                    rowId = ContentUris.parseId(result);
834                    entry.mRowId = rowId;
835                }
836            } else {
837                // updated file
838                result = ContentUris.withAppendedId(tableUri, rowId);
839                // path should never change, and we want to avoid replacing mixed cased paths
840                // with squashed lower case paths
841                values.remove(MediaStore.MediaColumns.DATA);
842                mMediaProvider.update(result, values, null, null);
843            }
844            if (mProcessGenres && mGenre != null) {
845                String genre = mGenre;
846                Uri uri = mGenreCache.get(genre);
847                if (uri == null) {
848                    Cursor cursor = null;
849                    try {
850                        // see if the genre already exists
851                        cursor = mMediaProvider.query(
852                                mGenresUri,
853                                GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?",
854                                        new String[] { genre }, null);
855                        if (cursor == null || cursor.getCount() == 0) {
856                            // genre does not exist, so create the genre in the genre table
857                            values.clear();
858                            values.put(MediaStore.Audio.Genres.NAME, genre);
859                            uri = mMediaProvider.insert(mGenresUri, values);
860                        } else {
861                            // genre already exists, so compute its Uri
862                            cursor.moveToNext();
863                            uri = ContentUris.withAppendedId(mGenresUri, cursor.getLong(0));
864                        }
865                        if (uri != null) {
866                            uri = Uri.withAppendedPath(uri, Genres.Members.CONTENT_DIRECTORY);
867                            mGenreCache.put(genre, uri);
868                        }
869                    } finally {
870                        // release the cursor if it exists
871                        if (cursor != null) {
872                            cursor.close();
873                        }
874                    }
875                }
876
877                if (uri != null) {
878                    // add entry to audio_genre_map
879                    values.clear();
880                    values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId));
881                    mMediaProvider.insert(uri, values);
882                }
883            }
884
885            if (notifications && !mDefaultNotificationSet) {
886                if (TextUtils.isEmpty(mDefaultNotificationFilename) ||
887                        doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) {
888                    setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
889                    mDefaultNotificationSet = true;
890                }
891            } else if (ringtones && !mDefaultRingtoneSet) {
892                if (TextUtils.isEmpty(mDefaultRingtoneFilename) ||
893                        doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) {
894                    setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
895                    mDefaultRingtoneSet = true;
896                }
897            } else if (alarms && !mDefaultAlarmSet) {
898                if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) ||
899                        doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) {
900                    setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
901                    mDefaultAlarmSet = true;
902                }
903            }
904
905            return result;
906        }
907
908        private boolean doesPathHaveFilename(String path, String filename) {
909            int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1;
910            int filenameLength = filename.length();
911            return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) &&
912                    pathFilenameStart + filenameLength == path.length();
913        }
914
915        private void setSettingIfNotSet(String settingName, Uri uri, long rowId) {
916
917            String existingSettingValue = Settings.System.getString(mContext.getContentResolver(),
918                    settingName);
919
920            if (TextUtils.isEmpty(existingSettingValue)) {
921                // Set the setting to the given URI
922                Settings.System.putString(mContext.getContentResolver(), settingName,
923                        ContentUris.withAppendedId(uri, rowId).toString());
924            }
925        }
926
927        private int getFileTypeFromDrm(String path) {
928            if (!isDrmEnabled()) {
929                return 0;
930            }
931
932            int resultFileType = 0;
933
934            if (mDrmManagerClient == null) {
935                mDrmManagerClient = new DrmManagerClient(mContext);
936            }
937
938            if (mDrmManagerClient.canHandle(path, null)) {
939                String drmMimetype = mDrmManagerClient.getOriginalMimeType(path);
940                if (drmMimetype != null) {
941                    mMimeType = drmMimetype;
942                    resultFileType = MediaFile.getFileTypeForMimeType(drmMimetype);
943                }
944            }
945            return resultFileType;
946        }
947
948    }; // end of anonymous MediaScannerClient instance
949
950    private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
951        Cursor c = null;
952        String where = null;
953        String[] selectionArgs = null;
954
955        if (mFileCache == null) {
956            mFileCache = new HashMap<String, FileCacheEntry>();
957        } else {
958            mFileCache.clear();
959        }
960        if (mPlayLists == null) {
961            mPlayLists = new ArrayList<FileCacheEntry>();
962        } else {
963            mPlayLists.clear();
964        }
965
966        if (filePath != null) {
967            // query for only one file
968            where = Files.FileColumns.DATA + "=?";
969            selectionArgs = new String[] { filePath };
970        }
971
972        // Build the list of files from the content provider
973        try {
974            if (prescanFiles) {
975                // First read existing files from the files table
976
977                c = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION,
978                        where, selectionArgs, null);
979
980                if (c != null) {
981                    while (c.moveToNext()) {
982                        long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
983                        String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
984                        int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
985                        long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
986
987                        // Only consider entries with absolute path names.
988                        // This allows storing URIs in the database without the
989                        // media scanner removing them.
990                        if (path != null && path.startsWith("/")) {
991                            String key = path;
992                            if (mCaseInsensitivePaths) {
993                                key = path.toLowerCase();
994                            }
995
996                            FileCacheEntry entry = new FileCacheEntry(rowId, path,
997                                    lastModified, format);
998                            mFileCache.put(key, entry);
999                        }
1000                    }
1001                    c.close();
1002                    c = null;
1003                }
1004            }
1005        }
1006        finally {
1007            if (c != null) {
1008                c.close();
1009            }
1010        }
1011
1012        // compute original size of images
1013        mOriginalCount = 0;
1014        c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null);
1015        if (c != null) {
1016            mOriginalCount = c.getCount();
1017            c.close();
1018        }
1019    }
1020
1021    private boolean inScanDirectory(String path, String[] directories) {
1022        for (int i = 0; i < directories.length; i++) {
1023            String directory = directories[i];
1024            if (path.startsWith(directory)) {
1025                return true;
1026            }
1027        }
1028        return false;
1029    }
1030
1031    private void pruneDeadThumbnailFiles() {
1032        HashSet<String> existingFiles = new HashSet<String>();
1033        String directory = "/sdcard/DCIM/.thumbnails";
1034        String [] files = (new File(directory)).list();
1035        if (files == null)
1036            files = new String[0];
1037
1038        for (int i = 0; i < files.length; i++) {
1039            String fullPathString = directory + "/" + files[i];
1040            existingFiles.add(fullPathString);
1041        }
1042
1043        try {
1044            Cursor c = mMediaProvider.query(
1045                    mThumbsUri,
1046                    new String [] { "_data" },
1047                    null,
1048                    null,
1049                    null);
1050            Log.v(TAG, "pruneDeadThumbnailFiles... " + c);
1051            if (c != null && c.moveToFirst()) {
1052                do {
1053                    String fullPathString = c.getString(0);
1054                    existingFiles.remove(fullPathString);
1055                } while (c.moveToNext());
1056            }
1057
1058            for (String fileToDelete : existingFiles) {
1059                if (false)
1060                    Log.v(TAG, "fileToDelete is " + fileToDelete);
1061                try {
1062                    (new File(fileToDelete)).delete();
1063                } catch (SecurityException ex) {
1064                }
1065            }
1066
1067            Log.v(TAG, "/pruneDeadThumbnailFiles... " + c);
1068            if (c != null) {
1069                c.close();
1070            }
1071        } catch (RemoteException e) {
1072            // We will soon be killed...
1073        }
1074    }
1075
1076    private void postscan(String[] directories) throws RemoteException {
1077        Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();
1078
1079        while (iterator.hasNext()) {
1080            FileCacheEntry entry = iterator.next();
1081            String path = entry.mPath;
1082
1083            // remove database entries for files that no longer exist.
1084            boolean fileMissing = false;
1085
1086            if (!entry.mSeenInFileSystem && !MtpConstants.isAbstractObject(entry.mFormat)) {
1087                if (inScanDirectory(path, directories)) {
1088                    // we didn't see this file in the scan directory.
1089                    fileMissing = true;
1090                } else {
1091                    // the file actually a directory or other abstract object
1092                    // or is outside of our scan directory,
1093                    // so we need to check for file existence here.
1094                    File testFile = new File(path);
1095                    if (!testFile.exists()) {
1096                        fileMissing = true;
1097                    }
1098                }
1099            }
1100
1101            if (fileMissing) {
1102                // do not delete missing playlists, since they may have been modified by the user.
1103                // the user can delete them in the media player instead.
1104                // instead, clear the path and lastModified fields in the row
1105                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1106                int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1107
1108                if (MediaFile.isPlayListFileType(fileType)) {
1109                    ContentValues values = new ContentValues();
1110                    values.put(MediaStore.Audio.Playlists.DATA, "");
1111                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, 0);
1112                    mMediaProvider.update(ContentUris.withAppendedId(mPlaylistsUri, entry.mRowId),
1113                            values, null, null);
1114                } else {
1115                    mMediaProvider.delete(ContentUris.withAppendedId(mFilesUri, entry.mRowId),
1116                            null, null);
1117                    iterator.remove();
1118                }
1119            }
1120        }
1121
1122        // handle playlists last, after we know what media files are on the storage.
1123        if (mProcessPlaylists) {
1124            processPlayLists();
1125        }
1126
1127        if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
1128            pruneDeadThumbnailFiles();
1129
1130        // allow GC to clean up
1131        mGenreCache = null;
1132        mPlayLists = null;
1133        mFileCache = null;
1134        mMediaProvider = null;
1135    }
1136
1137    private void initialize(String volumeName) {
1138        mMediaProvider = mContext.getContentResolver().acquireProvider("media");
1139
1140        mAudioUri = Audio.Media.getContentUri(volumeName);
1141        mVideoUri = Video.Media.getContentUri(volumeName);
1142        mImagesUri = Images.Media.getContentUri(volumeName);
1143        mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
1144        mFilesUri = Files.getContentUri(volumeName);
1145
1146        if (!volumeName.equals("internal")) {
1147            // we only support playlists on external media
1148            mProcessPlaylists = true;
1149            mProcessGenres = true;
1150            mGenreCache = new HashMap<String, Uri>();
1151            mGenresUri = Genres.getContentUri(volumeName);
1152            mPlaylistsUri = Playlists.getContentUri(volumeName);
1153
1154            mCaseInsensitivePaths = true;
1155        }
1156    }
1157
1158    public void scanDirectories(String[] directories, String volumeName) {
1159        try {
1160            long start = System.currentTimeMillis();
1161            initialize(volumeName);
1162            prescan(null, true);
1163            long prescan = System.currentTimeMillis();
1164
1165            for (int i = 0; i < directories.length; i++) {
1166                processDirectory(directories[i], mClient);
1167            }
1168            long scan = System.currentTimeMillis();
1169            postscan(directories);
1170            long end = System.currentTimeMillis();
1171
1172            if (false) {
1173                Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n");
1174                Log.d(TAG, "    scan time: " + (scan - prescan) + "ms\n");
1175                Log.d(TAG, "postscan time: " + (end - scan) + "ms\n");
1176                Log.d(TAG, "   total time: " + (end - start) + "ms\n");
1177            }
1178        } catch (SQLException e) {
1179            // this might happen if the SD card is removed while the media scanner is running
1180            Log.e(TAG, "SQLException in MediaScanner.scan()", e);
1181        } catch (UnsupportedOperationException e) {
1182            // this might happen if the SD card is removed while the media scanner is running
1183            Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e);
1184        } catch (RemoteException e) {
1185            Log.e(TAG, "RemoteException in MediaScanner.scan()", e);
1186        }
1187    }
1188
1189    // this function is used to scan a single file
1190    public Uri scanSingleFile(String path, String volumeName, String mimeType) {
1191        try {
1192            initialize(volumeName);
1193            prescan(path, true);
1194
1195            File file = new File(path);
1196
1197            // lastModified is in milliseconds on Files.
1198            long lastModifiedSeconds = file.lastModified() / 1000;
1199
1200            // always scan the file, so we can return the content://media Uri for existing files
1201            return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(),
1202                    false, true, false);
1203        } catch (RemoteException e) {
1204            Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1205            return null;
1206        }
1207    }
1208
1209    private static boolean isNoMediaFile(String path) {
1210        File file = new File(path);
1211        if (file.isDirectory()) return false;
1212
1213        // special case certain file names
1214        // I use regionMatches() instead of substring() below
1215        // to avoid memory allocation
1216        int lastSlash = path.lastIndexOf('/');
1217        if (lastSlash >= 0 && lastSlash + 2 < path.length()) {
1218            // ignore those ._* files created by MacOS
1219            if (path.regionMatches(lastSlash + 1, "._", 0, 2)) {
1220                return true;
1221            }
1222
1223            // ignore album art files created by Windows Media Player:
1224            // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg
1225            // and AlbumArt_{...}_Small.jpg
1226            if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) {
1227                if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) ||
1228                        path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) {
1229                    return true;
1230                }
1231                int length = path.length() - lastSlash - 1;
1232                if ((length == 17 && path.regionMatches(
1233                        true, lastSlash + 1, "AlbumArtSmall", 0, 13)) ||
1234                        (length == 10
1235                         && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) {
1236                    return true;
1237                }
1238            }
1239        }
1240        return false;
1241    }
1242
1243    public static boolean isNoMediaPath(String path) {
1244        if (path == null) return false;
1245
1246        // return true if file or any parent directory has name starting with a dot
1247        if (path.indexOf("/.") >= 0) return true;
1248
1249        // now check to see if any parent directories have a ".nomedia" file
1250        // start from 1 so we don't bother checking in the root directory
1251        int offset = 1;
1252        while (offset >= 0) {
1253            int slashIndex = path.indexOf('/', offset);
1254            if (slashIndex > offset) {
1255                slashIndex++; // move past slash
1256                File file = new File(path.substring(0, slashIndex) + ".nomedia");
1257                if (file.exists()) {
1258                    // we have a .nomedia in one of the parent directories
1259                    return true;
1260                }
1261            }
1262            offset = slashIndex;
1263        }
1264        return isNoMediaFile(path);
1265    }
1266
1267    public void scanMtpFile(String path, String volumeName, int objectHandle, int format) {
1268        initialize(volumeName);
1269        MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1270        int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1271        File file = new File(path);
1272        long lastModifiedSeconds = file.lastModified() / 1000;
1273
1274        if (!MediaFile.isAudioFileType(fileType) && !MediaFile.isVideoFileType(fileType) &&
1275            !MediaFile.isImageFileType(fileType) && !MediaFile.isPlayListFileType(fileType)) {
1276
1277            // no need to use the media scanner, but we need to update last modified and file size
1278            ContentValues values = new ContentValues();
1279            values.put(Files.FileColumns.SIZE, file.length());
1280            values.put(Files.FileColumns.DATE_MODIFIED, lastModifiedSeconds);
1281            try {
1282                String[] whereArgs = new String[] {  Integer.toString(objectHandle) };
1283                mMediaProvider.update(Files.getMtpObjectsUri(volumeName), values, "_id=?",
1284                        whereArgs);
1285            } catch (RemoteException e) {
1286                Log.e(TAG, "RemoteException in scanMtpFile", e);
1287            }
1288            return;
1289        }
1290
1291        mMtpObjectHandle = objectHandle;
1292        try {
1293            if (MediaFile.isPlayListFileType(fileType)) {
1294                // build file cache so we can look up tracks in the playlist
1295                prescan(null, true);
1296
1297                String key = path;
1298                if (mCaseInsensitivePaths) {
1299                    key = path.toLowerCase();
1300                }
1301                FileCacheEntry entry = mFileCache.get(key);
1302                if (entry != null) {
1303                    processPlayList(entry);
1304                }
1305            } else {
1306                // MTP will create a file entry for us so we don't want to do it in prescan
1307                prescan(path, false);
1308
1309                // always scan the file, so we can return the content://media Uri for existing files
1310                mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(),
1311                    (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path));
1312            }
1313        } catch (RemoteException e) {
1314            Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1315        } finally {
1316            mMtpObjectHandle = 0;
1317        }
1318    }
1319
1320    // returns the number of matching file/directory names, starting from the right
1321    private int matchPaths(String path1, String path2) {
1322        int result = 0;
1323        int end1 = path1.length();
1324        int end2 = path2.length();
1325
1326        while (end1 > 0 && end2 > 0) {
1327            int slash1 = path1.lastIndexOf('/', end1 - 1);
1328            int slash2 = path2.lastIndexOf('/', end2 - 1);
1329            int backSlash1 = path1.lastIndexOf('\\', end1 - 1);
1330            int backSlash2 = path2.lastIndexOf('\\', end2 - 1);
1331            int start1 = (slash1 > backSlash1 ? slash1 : backSlash1);
1332            int start2 = (slash2 > backSlash2 ? slash2 : backSlash2);
1333            if (start1 < 0) start1 = 0; else start1++;
1334            if (start2 < 0) start2 = 0; else start2++;
1335            int length = end1 - start1;
1336            if (end2 - start2 != length) break;
1337            if (path1.regionMatches(true, start1, path2, start2, length)) {
1338                result++;
1339                end1 = start1 - 1;
1340                end2 = start2 - 1;
1341            } else break;
1342        }
1343
1344        return result;
1345    }
1346
1347    private boolean addPlayListEntry(String entry, String playListDirectory,
1348            Uri uri, ContentValues values, int index) {
1349
1350        // watch for trailing whitespace
1351        int entryLength = entry.length();
1352        while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--;
1353        // path should be longer than 3 characters.
1354        // avoid index out of bounds errors below by returning here.
1355        if (entryLength < 3) return false;
1356        if (entryLength < entry.length()) entry = entry.substring(0, entryLength);
1357
1358        // does entry appear to be an absolute path?
1359        // look for Unix or DOS absolute paths
1360        char ch1 = entry.charAt(0);
1361        boolean fullPath = (ch1 == '/' ||
1362                (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\'));
1363        // if we have a relative path, combine entry with playListDirectory
1364        if (!fullPath)
1365            entry = playListDirectory + entry;
1366
1367        //FIXME - should we look for "../" within the path?
1368
1369        // best matching MediaFile for the play list entry
1370        FileCacheEntry bestMatch = null;
1371
1372        // number of rightmost file/directory names for bestMatch
1373        int bestMatchLength = 0;
1374
1375        Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();
1376        while (iterator.hasNext()) {
1377            FileCacheEntry cacheEntry = iterator.next();
1378            String path = cacheEntry.mPath;
1379
1380            if (path.equalsIgnoreCase(entry)) {
1381                bestMatch = cacheEntry;
1382                break;    // don't bother continuing search
1383            }
1384
1385            int matchLength = matchPaths(path, entry);
1386            if (matchLength > bestMatchLength) {
1387                bestMatch = cacheEntry;
1388                bestMatchLength = matchLength;
1389            }
1390        }
1391
1392        if (bestMatch == null) {
1393            return false;
1394        }
1395
1396        try {
1397        // OK, now we need to add this to the database
1398            values.clear();
1399            values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index));
1400            values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId));
1401            mMediaProvider.insert(uri, values);
1402        } catch (RemoteException e) {
1403            Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e);
1404            return false;
1405        }
1406
1407        return true;
1408    }
1409
1410    private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) {
1411        BufferedReader reader = null;
1412        try {
1413            File f = new File(path);
1414            if (f.exists()) {
1415                reader = new BufferedReader(
1416                        new InputStreamReader(new FileInputStream(f)), 8192);
1417                String line = reader.readLine();
1418                int index = 0;
1419                while (line != null) {
1420                    // ignore comment lines, which begin with '#'
1421                    if (line.length() > 0 && line.charAt(0) != '#') {
1422                        values.clear();
1423                        if (addPlayListEntry(line, playListDirectory, uri, values, index))
1424                            index++;
1425                    }
1426                    line = reader.readLine();
1427                }
1428            }
1429        } catch (IOException e) {
1430            Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1431        } finally {
1432            try {
1433                if (reader != null)
1434                    reader.close();
1435            } catch (IOException e) {
1436                Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1437            }
1438        }
1439    }
1440
1441    private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) {
1442        BufferedReader reader = null;
1443        try {
1444            File f = new File(path);
1445            if (f.exists()) {
1446                reader = new BufferedReader(
1447                        new InputStreamReader(new FileInputStream(f)), 8192);
1448                String line = reader.readLine();
1449                int index = 0;
1450                while (line != null) {
1451                    // ignore comment lines, which begin with '#'
1452                    if (line.startsWith("File")) {
1453                        int equals = line.indexOf('=');
1454                        if (equals > 0) {
1455                            values.clear();
1456                            if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index))
1457                                index++;
1458                        }
1459                    }
1460                    line = reader.readLine();
1461                }
1462            }
1463        } catch (IOException e) {
1464            Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1465        } finally {
1466            try {
1467                if (reader != null)
1468                    reader.close();
1469            } catch (IOException e) {
1470                Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1471            }
1472        }
1473    }
1474
1475    class WplHandler implements ElementListener {
1476
1477        final ContentHandler handler;
1478        String playListDirectory;
1479        Uri uri;
1480        ContentValues values = new ContentValues();
1481        int index = 0;
1482
1483        public WplHandler(String playListDirectory, Uri uri) {
1484            this.playListDirectory = playListDirectory;
1485            this.uri = uri;
1486
1487            RootElement root = new RootElement("smil");
1488            Element body = root.getChild("body");
1489            Element seq = body.getChild("seq");
1490            Element media = seq.getChild("media");
1491            media.setElementListener(this);
1492
1493            this.handler = root.getContentHandler();
1494        }
1495
1496        public void start(Attributes attributes) {
1497            String path = attributes.getValue("", "src");
1498            if (path != null) {
1499                values.clear();
1500                if (addPlayListEntry(path, playListDirectory, uri, values, index)) {
1501                    index++;
1502                }
1503            }
1504        }
1505
1506       public void end() {
1507       }
1508
1509        ContentHandler getContentHandler() {
1510            return handler;
1511        }
1512    }
1513
1514    private void processWplPlayList(String path, String playListDirectory, Uri uri) {
1515        FileInputStream fis = null;
1516        try {
1517            File f = new File(path);
1518            if (f.exists()) {
1519                fis = new FileInputStream(f);
1520
1521                Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler());
1522            }
1523        } catch (SAXException e) {
1524            e.printStackTrace();
1525        } catch (IOException e) {
1526            e.printStackTrace();
1527        } finally {
1528            try {
1529                if (fis != null)
1530                    fis.close();
1531            } catch (IOException e) {
1532                Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e);
1533            }
1534        }
1535    }
1536
1537    private void processPlayList(FileCacheEntry entry) throws RemoteException {
1538        String path = entry.mPath;
1539        ContentValues values = new ContentValues();
1540        int lastSlash = path.lastIndexOf('/');
1541        if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path);
1542        Uri uri, membersUri;
1543        long rowId = entry.mRowId;
1544
1545        // make sure we have a name
1546        String name = values.getAsString(MediaStore.Audio.Playlists.NAME);
1547        if (name == null) {
1548            name = values.getAsString(MediaStore.MediaColumns.TITLE);
1549            if (name == null) {
1550                // extract name from file name
1551                int lastDot = path.lastIndexOf('.');
1552                name = (lastDot < 0 ? path.substring(lastSlash + 1)
1553                        : path.substring(lastSlash + 1, lastDot));
1554            }
1555        }
1556
1557        values.put(MediaStore.Audio.Playlists.NAME, name);
1558        values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1559
1560        if (rowId == 0) {
1561            values.put(MediaStore.Audio.Playlists.DATA, path);
1562            uri = mMediaProvider.insert(mPlaylistsUri, values);
1563            rowId = ContentUris.parseId(uri);
1564            membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1565        } else {
1566            uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
1567            mMediaProvider.update(uri, values, null, null);
1568
1569            // delete members of existing playlist
1570            membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1571            mMediaProvider.delete(membersUri, null, null);
1572        }
1573
1574        String playListDirectory = path.substring(0, lastSlash + 1);
1575        MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1576        int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1577
1578        if (fileType == MediaFile.FILE_TYPE_M3U) {
1579            processM3uPlayList(path, playListDirectory, membersUri, values);
1580        } else if (fileType == MediaFile.FILE_TYPE_PLS) {
1581            processPlsPlayList(path, playListDirectory, membersUri, values);
1582        } else if (fileType == MediaFile.FILE_TYPE_WPL) {
1583            processWplPlayList(path, playListDirectory, membersUri);
1584        }
1585    }
1586
1587    private void processPlayLists() throws RemoteException {
1588        Iterator<FileCacheEntry> iterator = mPlayLists.iterator();
1589        while (iterator.hasNext()) {
1590            FileCacheEntry entry = iterator.next();
1591            // only process playlist files if they are new or have been modified since the last scan
1592            if (entry.mLastModifiedChanged) {
1593                processPlayList(entry);
1594            }
1595        }
1596    }
1597
1598    private native void processDirectory(String path, MediaScannerClient client);
1599    private native void processFile(String path, String mimeType, MediaScannerClient client);
1600    public native void setLocale(String locale);
1601
1602    public native byte[] extractAlbumArt(FileDescriptor fd);
1603
1604    private static native final void native_init();
1605    private native final void native_setup();
1606    private native final void native_finalize();
1607
1608    /**
1609     * Releases resouces associated with this MediaScanner object.
1610     * It is considered good practice to call this method when
1611     * one is done using the MediaScanner object. After this method
1612     * is called, the MediaScanner object can no longer be used.
1613     */
1614    public void release() {
1615        native_finalize();
1616    }
1617
1618    @Override
1619    protected void finalize() {
1620        mContext.getContentResolver().releaseProvider(mMediaProvider);
1621        native_finalize();
1622    }
1623}
1624