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