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