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