MediaScanner.java revision 45aaa2a345292bb6d600822db602f25e261c0e50
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 * The MediaScanner class is not thread-safe, so it should only be used in a single threaded manner.
105 *
106 * {@hide}
107 */
108public class MediaScanner
109{
110    static {
111        System.loadLibrary("media_jni");
112        native_init();
113    }
114
115    private final static String TAG = "MediaScanner";
116
117    private static final String[] FILES_PRESCAN_PROJECTION = new String[] {
118            Files.FileColumns._ID, // 0
119            Files.FileColumns.DATA, // 1
120            Files.FileColumns.FORMAT, // 2
121            Files.FileColumns.DATE_MODIFIED, // 3
122    };
123
124    private static final String[] ID_PROJECTION = new String[] {
125            Files.FileColumns._ID,
126    };
127
128    private static final int FILES_PRESCAN_ID_COLUMN_INDEX = 0;
129    private static final int FILES_PRESCAN_PATH_COLUMN_INDEX = 1;
130    private static final int FILES_PRESCAN_FORMAT_COLUMN_INDEX = 2;
131    private static final int FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX = 3;
132
133    private static final String[] PLAYLIST_MEMBERS_PROJECTION = new String[] {
134            Audio.Playlists.Members.PLAYLIST_ID, // 0
135     };
136
137    private static final int ID_PLAYLISTS_COLUMN_INDEX = 0;
138    private static final int PATH_PLAYLISTS_COLUMN_INDEX = 1;
139    private static final int DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX = 2;
140
141    private static final String[] GENRE_LOOKUP_PROJECTION = new String[] {
142            Audio.Genres._ID, // 0
143            Audio.Genres.NAME, // 1
144    };
145
146    private static final String RINGTONES_DIR = "/ringtones/";
147    private static final String NOTIFICATIONS_DIR = "/notifications/";
148    private static final String ALARMS_DIR = "/alarms/";
149    private static final String MUSIC_DIR = "/music/";
150    private static final String PODCAST_DIR = "/podcasts/";
151
152    private static final String[] ID3_GENRES = {
153        // ID3v1 Genres
154        "Blues",
155        "Classic Rock",
156        "Country",
157        "Dance",
158        "Disco",
159        "Funk",
160        "Grunge",
161        "Hip-Hop",
162        "Jazz",
163        "Metal",
164        "New Age",
165        "Oldies",
166        "Other",
167        "Pop",
168        "R&B",
169        "Rap",
170        "Reggae",
171        "Rock",
172        "Techno",
173        "Industrial",
174        "Alternative",
175        "Ska",
176        "Death Metal",
177        "Pranks",
178        "Soundtrack",
179        "Euro-Techno",
180        "Ambient",
181        "Trip-Hop",
182        "Vocal",
183        "Jazz+Funk",
184        "Fusion",
185        "Trance",
186        "Classical",
187        "Instrumental",
188        "Acid",
189        "House",
190        "Game",
191        "Sound Clip",
192        "Gospel",
193        "Noise",
194        "AlternRock",
195        "Bass",
196        "Soul",
197        "Punk",
198        "Space",
199        "Meditative",
200        "Instrumental Pop",
201        "Instrumental Rock",
202        "Ethnic",
203        "Gothic",
204        "Darkwave",
205        "Techno-Industrial",
206        "Electronic",
207        "Pop-Folk",
208        "Eurodance",
209        "Dream",
210        "Southern Rock",
211        "Comedy",
212        "Cult",
213        "Gangsta",
214        "Top 40",
215        "Christian Rap",
216        "Pop/Funk",
217        "Jungle",
218        "Native American",
219        "Cabaret",
220        "New Wave",
221        "Psychadelic",
222        "Rave",
223        "Showtunes",
224        "Trailer",
225        "Lo-Fi",
226        "Tribal",
227        "Acid Punk",
228        "Acid Jazz",
229        "Polka",
230        "Retro",
231        "Musical",
232        "Rock & Roll",
233        "Hard Rock",
234        // The following genres are Winamp extensions
235        "Folk",
236        "Folk-Rock",
237        "National Folk",
238        "Swing",
239        "Fast Fusion",
240        "Bebob",
241        "Latin",
242        "Revival",
243        "Celtic",
244        "Bluegrass",
245        "Avantgarde",
246        "Gothic Rock",
247        "Progressive Rock",
248        "Psychedelic Rock",
249        "Symphonic Rock",
250        "Slow Rock",
251        "Big Band",
252        "Chorus",
253        "Easy Listening",
254        "Acoustic",
255        "Humour",
256        "Speech",
257        "Chanson",
258        "Opera",
259        "Chamber Music",
260        "Sonata",
261        "Symphony",
262        "Booty Bass",
263        "Primus",
264        "Porn Groove",
265        "Satire",
266        "Slow Jam",
267        "Club",
268        "Tango",
269        "Samba",
270        "Folklore",
271        "Ballad",
272        "Power Ballad",
273        "Rhythmic Soul",
274        "Freestyle",
275        "Duet",
276        "Punk Rock",
277        "Drum Solo",
278        "A capella",
279        "Euro-House",
280        "Dance Hall",
281        // The following ones seem to be fairly widely supported as well
282        "Goa",
283        "Drum & Bass",
284        "Club-House",
285        "Hardcore",
286        "Terror",
287        "Indie",
288        "Britpop",
289        "Negerpunk",
290        "Polsk Punk",
291        "Beat",
292        "Christian Gangsta",
293        "Heavy Metal",
294        "Black Metal",
295        "Crossover",
296        "Contemporary Christian",
297        "Christian Rock",
298        "Merengue",
299        "Salsa",
300        "Thrash Metal",
301        "Anime",
302        "JPop",
303        "Synthpop",
304        // 148 and up don't seem to have been defined yet.
305    };
306
307    private int mNativeContext;
308    private Context mContext;
309    private IContentProvider mMediaProvider;
310    private Uri mAudioUri;
311    private Uri mVideoUri;
312    private Uri mImagesUri;
313    private Uri mThumbsUri;
314    private Uri mGenresUri;
315    private Uri mPlaylistsUri;
316    private Uri mFilesUri;
317    private boolean mProcessPlaylists, mProcessGenres;
318    private int mMtpObjectHandle;
319
320    private final String mExternalStoragePath;
321
322    // used when scanning the image database so we know whether we have to prune
323    // old thumbnail files
324    private int mOriginalCount;
325    /** Whether the scanner has set a default sound for the ringer ringtone. */
326    private boolean mDefaultRingtoneSet;
327    /** Whether the scanner has set a default sound for the notification ringtone. */
328    private boolean mDefaultNotificationSet;
329    /** Whether the scanner has set a default sound for the alarm ringtone. */
330    private boolean mDefaultAlarmSet;
331    /** The filename for the default sound for the ringer ringtone. */
332    private String mDefaultRingtoneFilename;
333    /** The filename for the default sound for the notification ringtone. */
334    private String mDefaultNotificationFilename;
335    /** The filename for the default sound for the alarm ringtone. */
336    private String mDefaultAlarmAlertFilename;
337    /**
338     * The prefix for system properties that define the default sound for
339     * ringtones. Concatenate the name of the setting from Settings
340     * to get the full system property.
341     */
342    private static final String DEFAULT_RINGTONE_PROPERTY_PREFIX = "ro.config.";
343
344    // set to true if file path comparisons should be case insensitive.
345    // this should be set when scanning files on a case insensitive file system.
346    private boolean mCaseInsensitivePaths;
347
348    private BitmapFactory.Options mBitmapOptions = new BitmapFactory.Options();
349
350    private static class FileCacheEntry {
351        long mRowId;
352        String mPath;
353        long mLastModified;
354        int mFormat;
355        boolean mSeenInFileSystem;
356        boolean mLastModifiedChanged;
357
358        FileCacheEntry(long rowId, String path, long lastModified, int format) {
359            mRowId = rowId;
360            mPath = path;
361            mLastModified = lastModified;
362            mFormat = format;
363            mSeenInFileSystem = false;
364            mLastModifiedChanged = false;
365        }
366
367        @Override
368        public String toString() {
369            return mPath + " mRowId: " + mRowId;
370        }
371    }
372
373    private class FileInserter {
374
375        ContentValues[] mValues = new ContentValues[1000];
376        int mIndex = 0;
377
378        public Uri insert(ContentValues values) {
379            if (mIndex == mValues.length) {
380                flush();
381            }
382            mValues[mIndex++] = values;
383            // URI not needed when doing bulk inserts
384            return null;
385        }
386
387        public void flush() {
388            while (mIndex < mValues.length) {
389                mValues[mIndex++] = null;
390            }
391            try {
392                mMediaProvider.bulkInsert(mFilesUri, mValues);
393            } catch (RemoteException e) {
394                Log.e(TAG, "RemoteException in FileInserter.flush()", e);
395            }
396            mIndex = 0;
397        }
398    }
399    private FileInserter mFileInserter;
400
401    // hashes file path to FileCacheEntry.
402    // path should be lower case if mCaseInsensitivePaths is true
403    private HashMap<String, FileCacheEntry> mFileCache;
404
405    private ArrayList<FileCacheEntry> mPlayLists;
406    private HashMap<String, Uri> mGenreCache;
407
408    private DrmManagerClient mDrmManagerClient = null;
409
410    public MediaScanner(Context c) {
411        native_setup();
412        mContext = c;
413        mBitmapOptions.inSampleSize = 1;
414        mBitmapOptions.inJustDecodeBounds = true;
415
416        setDefaultRingtoneFileNames();
417
418        mExternalStoragePath = Environment.getExternalStorageDirectory().getAbsolutePath();
419    }
420
421    private void setDefaultRingtoneFileNames() {
422        mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
423                + Settings.System.RINGTONE);
424        mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
425                + Settings.System.NOTIFICATION_SOUND);
426        mDefaultAlarmAlertFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
427                + Settings.System.ALARM_ALERT);
428    }
429
430    private MyMediaScannerClient mClient = new MyMediaScannerClient();
431
432    private boolean isDrmEnabled() {
433        String prop = SystemProperties.get("drm.service.enabled");
434        return prop != null && prop.equals("true");
435    }
436
437    private class MyMediaScannerClient implements MediaScannerClient {
438
439        private String mArtist;
440        private String mAlbumArtist;    // use this if mArtist is missing
441        private String mAlbum;
442        private String mTitle;
443        private String mComposer;
444        private String mGenre;
445        private String mMimeType;
446        private int mFileType;
447        private int mTrack;
448        private int mYear;
449        private int mDuration;
450        private String mPath;
451        private long mLastModified;
452        private long mFileSize;
453        private String mWriter;
454        private int mCompilation;
455        private boolean mIsDrm;
456        private boolean mNoMedia;   // flag to suppress file from appearing in media tables
457
458        public FileCacheEntry beginFile(String path, String mimeType, long lastModified,
459                long fileSize, boolean isDirectory, boolean noMedia) {
460            mMimeType = mimeType;
461            mFileType = 0;
462            mFileSize = fileSize;
463
464            if (!isDirectory) {
465                if (!noMedia && isNoMediaFile(path)) {
466                    noMedia = true;
467                }
468                mNoMedia = noMedia;
469
470                // try mimeType first, if it is specified
471                if (mimeType != null) {
472                    mFileType = MediaFile.getFileTypeForMimeType(mimeType);
473                }
474
475                // if mimeType was not specified, compute file type based on file extension.
476                if (mFileType == 0) {
477                    MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
478                    if (mediaFileType != null) {
479                        mFileType = mediaFileType.fileType;
480                        if (mMimeType == null) {
481                            mMimeType = mediaFileType.mimeType;
482                        }
483                    }
484                }
485
486                if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) {
487                    mFileType = getFileTypeFromDrm(path);
488                }
489            }
490
491            String key = path;
492            if (mCaseInsensitivePaths) {
493                key = path.toLowerCase();
494            }
495            FileCacheEntry entry = mFileCache.get(key);
496            // add some slack to avoid a rounding error
497            long delta = (entry != null) ? (lastModified - entry.mLastModified) : 0;
498            boolean wasModified = delta > 1 || delta < -1;
499            if (entry == null || wasModified) {
500                if (wasModified) {
501                    entry.mLastModified = lastModified;
502                } else {
503                    entry = new FileCacheEntry(0, path, lastModified,
504                            (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
505                    mFileCache.put(key, entry);
506                }
507                entry.mLastModifiedChanged = true;
508            }
509            entry.mSeenInFileSystem = true;
510
511            if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
512                mPlayLists.add(entry);
513                // we don't process playlists in the main scan, so return null
514                return null;
515            }
516
517            // clear all the metadata
518            mArtist = null;
519            mAlbumArtist = null;
520            mAlbum = null;
521            mTitle = null;
522            mComposer = null;
523            mGenre = null;
524            mTrack = 0;
525            mYear = 0;
526            mDuration = 0;
527            mPath = path;
528            mLastModified = lastModified;
529            mWriter = null;
530            mCompilation = 0;
531            mIsDrm = false;
532
533            return entry;
534        }
535
536        public void scanFile(String path, long lastModified, long fileSize,
537                boolean isDirectory, boolean noMedia) {
538            // This is the callback funtion from native codes.
539            // Log.v(TAG, "scanFile: "+path);
540            doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
541        }
542
543        public Uri doScanFile(String path, String mimeType, long lastModified,
544                long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
545            Uri result = null;
546//            long t1 = System.currentTimeMillis();
547            try {
548                FileCacheEntry entry = beginFile(path, mimeType, lastModified,
549                        fileSize, isDirectory, noMedia);
550                // rescan for metadata if file was modified since last scan
551                if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
552                    if (noMedia) {
553                        result = endFile(entry, false, false, false, false, false);
554                    } else {
555                        String lowpath = path.toLowerCase();
556                        boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
557                        boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
558                        boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
559                        boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
560                        boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
561                            (!ringtones && !notifications && !alarms && !podcasts);
562
563                        // we only extract metadata for audio and video files
564                        if (MediaFile.isAudioFileType(mFileType)
565                                || MediaFile.isVideoFileType(mFileType)) {
566                            processFile(path, mimeType, this);
567                        }
568
569                        result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
570                    }
571                }
572            } catch (RemoteException e) {
573                Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
574            }
575//            long t2 = System.currentTimeMillis();
576//            Log.v(TAG, "scanFile: " + path + " took " + (t2-t1));
577            return result;
578        }
579
580        private int parseSubstring(String s, int start, int defaultValue) {
581            int length = s.length();
582            if (start == length) return defaultValue;
583
584            char ch = s.charAt(start++);
585            // return defaultValue if we have no integer at all
586            if (ch < '0' || ch > '9') return defaultValue;
587
588            int result = ch - '0';
589            while (start < length) {
590                ch = s.charAt(start++);
591                if (ch < '0' || ch > '9') return result;
592                result = result * 10 + (ch - '0');
593            }
594
595            return result;
596        }
597
598        public void handleStringTag(String name, String value) {
599            if (name.equalsIgnoreCase("title") || name.startsWith("title;")) {
600                // Don't trim() here, to preserve the special \001 character
601                // used to force sorting. The media provider will trim() before
602                // inserting the title in to the database.
603                mTitle = value;
604            } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) {
605                mArtist = value.trim();
606            } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")
607                    || name.equalsIgnoreCase("band") || name.startsWith("band;")) {
608                mAlbumArtist = value.trim();
609            } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) {
610                mAlbum = value.trim();
611            } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) {
612                mComposer = value.trim();
613            } else if (name.equalsIgnoreCase("genre") || name.startsWith("genre;")) {
614                mGenre = getGenreName(value);
615            } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) {
616                mYear = parseSubstring(value, 0, 0);
617            } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) {
618                // track number might be of the form "2/12"
619                // we just read the number before the slash
620                int num = parseSubstring(value, 0, 0);
621                mTrack = (mTrack / 1000) * 1000 + num;
622            } else if (name.equalsIgnoreCase("discnumber") ||
623                    name.equals("set") || name.startsWith("set;")) {
624                // set number might be of the form "1/3"
625                // we just read the number before the slash
626                int num = parseSubstring(value, 0, 0);
627                mTrack = (num * 1000) + (mTrack % 1000);
628            } else if (name.equalsIgnoreCase("duration")) {
629                mDuration = parseSubstring(value, 0, 0);
630            } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {
631                mWriter = value.trim();
632            } else if (name.equalsIgnoreCase("compilation")) {
633                mCompilation = parseSubstring(value, 0, 0);
634            } else if (name.equalsIgnoreCase("isdrm")) {
635                mIsDrm = (parseSubstring(value, 0, 0) == 1);
636            }
637        }
638
639        public String getGenreName(String genreTagValue) {
640
641            if (genreTagValue == null) {
642                return null;
643            }
644            final int length = genreTagValue.length();
645
646            if (length > 0 && genreTagValue.charAt(0) == '(') {
647                StringBuffer number = new StringBuffer();
648                int i = 1;
649                for (; i < length - 1; ++i) {
650                    char c = genreTagValue.charAt(i);
651                    if (Character.isDigit(c)) {
652                        number.append(c);
653                    } else {
654                        break;
655                    }
656                }
657                if (genreTagValue.charAt(i) == ')') {
658                    try {
659                        short genreIndex = Short.parseShort(number.toString());
660                        if (genreIndex >= 0) {
661                            if (genreIndex < ID3_GENRES.length) {
662                                return ID3_GENRES[genreIndex];
663                            } else if (genreIndex == 0xFF) {
664                                return null;
665                            } else if (genreIndex < 0xFF && (i + 1) < length) {
666                                // genre is valid but unknown,
667                                // if there is a string after the value we take it
668                                return genreTagValue.substring(i + 1);
669                            } else {
670                                // else return the number, without parentheses
671                                return number.toString();
672                            }
673                        }
674                    } catch (NumberFormatException e) {
675                    }
676                }
677            }
678
679            return genreTagValue;
680        }
681
682        public void setMimeType(String mimeType) {
683            if ("audio/mp4".equals(mMimeType) &&
684                    mimeType.startsWith("video")) {
685                // for feature parity with Donut, we force m4a files to keep the
686                // audio/mp4 mimetype, even if they are really "enhanced podcasts"
687                // with a video track
688                return;
689            }
690            mMimeType = mimeType;
691            mFileType = MediaFile.getFileTypeForMimeType(mimeType);
692        }
693
694        /**
695         * Formats the data into a values array suitable for use with the Media
696         * Content Provider.
697         *
698         * @return a map of values
699         */
700        private ContentValues toValues() {
701            ContentValues map = new ContentValues();
702
703            map.put(MediaStore.MediaColumns.DATA, mPath);
704            map.put(MediaStore.MediaColumns.TITLE, mTitle);
705            map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified);
706            map.put(MediaStore.MediaColumns.SIZE, mFileSize);
707            map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType);
708            map.put(MediaStore.MediaColumns.IS_DRM, mIsDrm);
709
710            if (!mNoMedia) {
711                if (MediaFile.isVideoFileType(mFileType)) {
712                    map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0
713                            ? mArtist : MediaStore.UNKNOWN_STRING));
714                    map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0
715                            ? mAlbum : MediaStore.UNKNOWN_STRING));
716                    map.put(Video.Media.DURATION, mDuration);
717                    // FIXME - add RESOLUTION
718                } else if (MediaFile.isImageFileType(mFileType)) {
719                    // FIXME - add DESCRIPTION
720                } else if (MediaFile.isAudioFileType(mFileType)) {
721                    map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ?
722                            mArtist : MediaStore.UNKNOWN_STRING);
723                    map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null &&
724                            mAlbumArtist.length() > 0) ? mAlbumArtist : null);
725                    map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ?
726                            mAlbum : MediaStore.UNKNOWN_STRING);
727                    map.put(Audio.Media.COMPOSER, mComposer);
728                    if (mYear != 0) {
729                        map.put(Audio.Media.YEAR, mYear);
730                    }
731                    map.put(Audio.Media.TRACK, mTrack);
732                    map.put(Audio.Media.DURATION, mDuration);
733                    map.put(Audio.Media.COMPILATION, mCompilation);
734                }
735            }
736            return map;
737        }
738
739        private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications,
740                boolean alarms, boolean music, boolean podcasts)
741                throws RemoteException {
742            // update database
743
744            // use album artist if artist is missing
745            if (mArtist == null || mArtist.length() == 0) {
746                mArtist = mAlbumArtist;
747            }
748
749            ContentValues values = toValues();
750            String title = values.getAsString(MediaStore.MediaColumns.TITLE);
751            if (title == null || TextUtils.isEmpty(title.trim())) {
752                title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA));
753                values.put(MediaStore.MediaColumns.TITLE, title);
754            }
755            String album = values.getAsString(Audio.Media.ALBUM);
756            if (MediaStore.UNKNOWN_STRING.equals(album)) {
757                album = values.getAsString(MediaStore.MediaColumns.DATA);
758                // extract last path segment before file name
759                int lastSlash = album.lastIndexOf('/');
760                if (lastSlash >= 0) {
761                    int previousSlash = 0;
762                    while (true) {
763                        int idx = album.indexOf('/', previousSlash + 1);
764                        if (idx < 0 || idx >= lastSlash) {
765                            break;
766                        }
767                        previousSlash = idx;
768                    }
769                    if (previousSlash != 0) {
770                        album = album.substring(previousSlash + 1, lastSlash);
771                        values.put(Audio.Media.ALBUM, album);
772                    }
773                }
774            }
775            long rowId = entry.mRowId;
776            if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) {
777                // Only set these for new entries. For existing entries, they
778                // may have been modified later, and we want to keep the current
779                // values so that custom ringtones still show up in the ringtone
780                // picker.
781                values.put(Audio.Media.IS_RINGTONE, ringtones);
782                values.put(Audio.Media.IS_NOTIFICATION, notifications);
783                values.put(Audio.Media.IS_ALARM, alarms);
784                values.put(Audio.Media.IS_MUSIC, music);
785                values.put(Audio.Media.IS_PODCAST, podcasts);
786            } else if (mFileType == MediaFile.FILE_TYPE_JPEG && !mNoMedia) {
787                ExifInterface exif = null;
788                try {
789                    exif = new ExifInterface(entry.mPath);
790                } catch (IOException ex) {
791                    // exif is null
792                }
793                if (exif != null) {
794                    float[] latlng = new float[2];
795                    if (exif.getLatLong(latlng)) {
796                        values.put(Images.Media.LATITUDE, latlng[0]);
797                        values.put(Images.Media.LONGITUDE, latlng[1]);
798                    }
799
800                    long time = exif.getGpsDateTime();
801                    if (time != -1) {
802                        values.put(Images.Media.DATE_TAKEN, time);
803                    } else {
804                        // If no time zone information is available, we should consider using
805                        // EXIF local time as taken time if the difference between file time
806                        // and EXIF local time is not less than 1 Day, otherwise MediaProvider
807                        // will use file time as taken time.
808                        time = exif.getDateTime();
809                        if (Math.abs(mLastModified * 1000 - time) >= 86400000) {
810                            values.put(Images.Media.DATE_TAKEN, time);
811                        }
812                    }
813
814                    int orientation = exif.getAttributeInt(
815                        ExifInterface.TAG_ORIENTATION, -1);
816                    if (orientation != -1) {
817                        // We only recognize a subset of orientation tag values.
818                        int degree;
819                        switch(orientation) {
820                            case ExifInterface.ORIENTATION_ROTATE_90:
821                                degree = 90;
822                                break;
823                            case ExifInterface.ORIENTATION_ROTATE_180:
824                                degree = 180;
825                                break;
826                            case ExifInterface.ORIENTATION_ROTATE_270:
827                                degree = 270;
828                                break;
829                            default:
830                                degree = 0;
831                                break;
832                        }
833                        values.put(Images.Media.ORIENTATION, degree);
834                    }
835                }
836            }
837
838            // For inserts we always use the file URI so we can insert in bulk.
839            // For updates we compute the URI based on the media type.
840            Uri tableUri = mFilesUri;
841            Uri result = null;
842            if (rowId == 0) {
843                if (mMtpObjectHandle != 0) {
844                    values.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle);
845                }
846                int format = entry.mFormat;
847                if (format == 0) {
848                    format = MediaFile.getFormatCode(entry.mPath, mMimeType);
849                }
850                values.put(Files.FileColumns.FORMAT, format);
851
852                // new file, insert it
853                if (mFileInserter != null) {
854                    result = mFileInserter.insert(values);
855                } else {
856                    result = mMediaProvider.insert(tableUri, values);
857                }
858
859                if (result != null) {
860                    rowId = ContentUris.parseId(result);
861                    entry.mRowId = rowId;
862                }
863            } else {
864                if (!mNoMedia) {
865                    if (MediaFile.isVideoFileType(mFileType)) {
866                        tableUri = mVideoUri;
867                    } else if (MediaFile.isImageFileType(mFileType)) {
868                        tableUri = mImagesUri;
869                    } else if (MediaFile.isAudioFileType(mFileType)) {
870                        tableUri = mAudioUri;
871                    }
872                }
873
874                // updated file
875                result = ContentUris.withAppendedId(tableUri, rowId);
876                // path should never change, and we want to avoid replacing mixed cased paths
877                // with squashed lower case paths
878                values.remove(MediaStore.MediaColumns.DATA);
879                mMediaProvider.update(result, values, null, null);
880            }
881            if (mProcessGenres && mGenre != null) {
882                String genre = mGenre;
883                Uri uri = mGenreCache.get(genre);
884                if (uri == null) {
885                    Cursor cursor = null;
886                    try {
887                        // see if the genre already exists
888                        cursor = mMediaProvider.query(
889                                mGenresUri,
890                                GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?",
891                                        new String[] { genre }, null);
892                        if (cursor == null || cursor.getCount() == 0) {
893                            // genre does not exist, so create the genre in the genre table
894                            values = new ContentValues();
895                            values.put(MediaStore.Audio.Genres.NAME, genre);
896                            uri = mMediaProvider.insert(mGenresUri, values);
897                        } else {
898                            // genre already exists, so compute its Uri
899                            cursor.moveToNext();
900                            uri = ContentUris.withAppendedId(mGenresUri, cursor.getLong(0));
901                        }
902                        if (uri != null) {
903                            uri = Uri.withAppendedPath(uri, Genres.Members.CONTENT_DIRECTORY);
904                            mGenreCache.put(genre, uri);
905                        }
906                    } finally {
907                        // release the cursor if it exists
908                        if (cursor != null) {
909                            cursor.close();
910                        }
911                    }
912                }
913
914                if (uri != null) {
915                    // add entry to audio_genre_map
916                    values = new ContentValues();
917                    values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId));
918                    mMediaProvider.insert(uri, values);
919                }
920            }
921
922            if (notifications && !mDefaultNotificationSet) {
923                if (TextUtils.isEmpty(mDefaultNotificationFilename) ||
924                        doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) {
925                    setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
926                    mDefaultNotificationSet = true;
927                }
928            } else if (ringtones && !mDefaultRingtoneSet) {
929                if (TextUtils.isEmpty(mDefaultRingtoneFilename) ||
930                        doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) {
931                    setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
932                    mDefaultRingtoneSet = true;
933                }
934            } else if (alarms && !mDefaultAlarmSet) {
935                if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) ||
936                        doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) {
937                    setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
938                    mDefaultAlarmSet = true;
939                }
940            }
941
942            return result;
943        }
944
945        private boolean doesPathHaveFilename(String path, String filename) {
946            int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1;
947            int filenameLength = filename.length();
948            return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) &&
949                    pathFilenameStart + filenameLength == path.length();
950        }
951
952        private void setSettingIfNotSet(String settingName, Uri uri, long rowId) {
953
954            String existingSettingValue = Settings.System.getString(mContext.getContentResolver(),
955                    settingName);
956
957            if (TextUtils.isEmpty(existingSettingValue)) {
958                // Set the setting to the given URI
959                Settings.System.putString(mContext.getContentResolver(), settingName,
960                        ContentUris.withAppendedId(uri, rowId).toString());
961            }
962        }
963
964        private int getFileTypeFromDrm(String path) {
965            if (!isDrmEnabled()) {
966                return 0;
967            }
968
969            int resultFileType = 0;
970
971            if (mDrmManagerClient == null) {
972                mDrmManagerClient = new DrmManagerClient(mContext);
973            }
974
975            if (mDrmManagerClient.canHandle(path, null)) {
976                String drmMimetype = mDrmManagerClient.getOriginalMimeType(path);
977                if (drmMimetype != null) {
978                    mMimeType = drmMimetype;
979                    resultFileType = MediaFile.getFileTypeForMimeType(drmMimetype);
980                }
981            }
982            return resultFileType;
983        }
984
985    }; // end of anonymous MediaScannerClient instance
986
987    private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
988        Cursor c = null;
989        String where = null;
990        String[] selectionArgs = null;
991
992        if (mFileCache == null) {
993            mFileCache = new HashMap<String, FileCacheEntry>();
994        } else {
995            mFileCache.clear();
996        }
997        if (mPlayLists == null) {
998            mPlayLists = new ArrayList<FileCacheEntry>();
999        } else {
1000            mPlayLists.clear();
1001        }
1002
1003        if (filePath != null) {
1004            // query for only one file
1005            where = Files.FileColumns.DATA + "=?";
1006            selectionArgs = new String[] { filePath };
1007        }
1008
1009        // Build the list of files from the content provider
1010        try {
1011            if (prescanFiles) {
1012                // First read existing files from the files table
1013
1014                c = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION,
1015                        where, selectionArgs, null);
1016
1017                if (c != null) {
1018                    while (c.moveToNext()) {
1019                        long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
1020                        String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
1021                        int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
1022                        long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
1023
1024                        // Only consider entries with absolute path names.
1025                        // This allows storing URIs in the database without the
1026                        // media scanner removing them.
1027                        if (path != null && path.startsWith("/")) {
1028                            String key = path;
1029                            if (mCaseInsensitivePaths) {
1030                                key = path.toLowerCase();
1031                            }
1032
1033                            FileCacheEntry entry = new FileCacheEntry(rowId, path,
1034                                    lastModified, format);
1035                            mFileCache.put(key, entry);
1036                        }
1037                    }
1038                    c.close();
1039                    c = null;
1040                }
1041            }
1042        }
1043        finally {
1044            if (c != null) {
1045                c.close();
1046            }
1047        }
1048
1049        // compute original size of images
1050        mOriginalCount = 0;
1051        c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null);
1052        if (c != null) {
1053            mOriginalCount = c.getCount();
1054            c.close();
1055        }
1056    }
1057
1058    private boolean inScanDirectory(String path, String[] directories) {
1059        for (int i = 0; i < directories.length; i++) {
1060            String directory = directories[i];
1061            if (path.startsWith(directory)) {
1062                return true;
1063            }
1064        }
1065        return false;
1066    }
1067
1068    private void pruneDeadThumbnailFiles() {
1069        HashSet<String> existingFiles = new HashSet<String>();
1070        String directory = "/sdcard/DCIM/.thumbnails";
1071        String [] files = (new File(directory)).list();
1072        if (files == null)
1073            files = new String[0];
1074
1075        for (int i = 0; i < files.length; i++) {
1076            String fullPathString = directory + "/" + files[i];
1077            existingFiles.add(fullPathString);
1078        }
1079
1080        try {
1081            Cursor c = mMediaProvider.query(
1082                    mThumbsUri,
1083                    new String [] { "_data" },
1084                    null,
1085                    null,
1086                    null);
1087            Log.v(TAG, "pruneDeadThumbnailFiles... " + c);
1088            if (c != null && c.moveToFirst()) {
1089                do {
1090                    String fullPathString = c.getString(0);
1091                    existingFiles.remove(fullPathString);
1092                } while (c.moveToNext());
1093            }
1094
1095            for (String fileToDelete : existingFiles) {
1096                if (false)
1097                    Log.v(TAG, "fileToDelete is " + fileToDelete);
1098                try {
1099                    (new File(fileToDelete)).delete();
1100                } catch (SecurityException ex) {
1101                }
1102            }
1103
1104            Log.v(TAG, "/pruneDeadThumbnailFiles... " + c);
1105            if (c != null) {
1106                c.close();
1107            }
1108        } catch (RemoteException e) {
1109            // We will soon be killed...
1110        }
1111    }
1112
1113    private void postscan(String[] directories) throws RemoteException {
1114        Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();
1115
1116        while (iterator.hasNext()) {
1117            FileCacheEntry entry = iterator.next();
1118            String path = entry.mPath;
1119
1120            // remove database entries for files that no longer exist.
1121            boolean fileMissing = false;
1122
1123            if (!entry.mSeenInFileSystem && !MtpConstants.isAbstractObject(entry.mFormat)) {
1124                if (inScanDirectory(path, directories)) {
1125                    // we didn't see this file in the scan directory.
1126                    fileMissing = true;
1127                } else {
1128                    // the file actually a directory or other abstract object
1129                    // or is outside of our scan directory,
1130                    // so we need to check for file existence here.
1131                    File testFile = new File(path);
1132                    if (!testFile.exists()) {
1133                        fileMissing = true;
1134                    }
1135                }
1136            }
1137
1138            if (fileMissing) {
1139                // do not delete missing playlists, since they may have been modified by the user.
1140                // the user can delete them in the media player instead.
1141                // instead, clear the path and lastModified fields in the row
1142                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1143                int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1144
1145                if (MediaFile.isPlayListFileType(fileType)) {
1146                    ContentValues values = new ContentValues();
1147                    values.put(MediaStore.Audio.Playlists.DATA, "");
1148                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, 0);
1149                    mMediaProvider.update(ContentUris.withAppendedId(mPlaylistsUri, entry.mRowId),
1150                            values, null, null);
1151                } else {
1152                    mMediaProvider.delete(ContentUris.withAppendedId(mFilesUri, entry.mRowId),
1153                            null, null);
1154                    iterator.remove();
1155                }
1156            }
1157        }
1158
1159        // handle playlists last, after we know what media files are on the storage.
1160        if (mProcessPlaylists) {
1161            processPlayLists();
1162        }
1163
1164        if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
1165            pruneDeadThumbnailFiles();
1166
1167        // allow GC to clean up
1168        mGenreCache = null;
1169        mPlayLists = null;
1170        mFileCache = null;
1171        mMediaProvider = null;
1172    }
1173
1174    private void initialize(String volumeName) {
1175        mMediaProvider = mContext.getContentResolver().acquireProvider("media");
1176
1177        mAudioUri = Audio.Media.getContentUri(volumeName);
1178        mVideoUri = Video.Media.getContentUri(volumeName);
1179        mImagesUri = Images.Media.getContentUri(volumeName);
1180        mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
1181        mFilesUri = Files.getContentUri(volumeName);
1182
1183        if (!volumeName.equals("internal")) {
1184            // we only support playlists on external media
1185            mProcessPlaylists = true;
1186            mProcessGenres = true;
1187            mGenreCache = new HashMap<String, Uri>();
1188            mGenresUri = Genres.getContentUri(volumeName);
1189            mPlaylistsUri = Playlists.getContentUri(volumeName);
1190
1191            mCaseInsensitivePaths = true;
1192        }
1193    }
1194
1195    public void scanDirectories(String[] directories, String volumeName) {
1196        try {
1197            long start = System.currentTimeMillis();
1198            initialize(volumeName);
1199            prescan(null, true);
1200            long prescan = System.currentTimeMillis();
1201            mFileInserter = new FileInserter();
1202
1203            for (int i = 0; i < directories.length; i++) {
1204                processDirectory(directories[i], mClient);
1205            }
1206            mFileInserter.flush();
1207            mFileInserter = null;
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