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