MediaScanner.java revision 9ff4774cac499a376a59373d5bfb5112c9a2a004
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.MediaStore.Files.FileColumns;
39import android.provider.Settings;
40import android.provider.MediaStore.Audio;
41import android.provider.MediaStore.Files;
42import android.provider.MediaStore.Images;
43import android.provider.MediaStore.Video;
44import android.provider.MediaStore.Audio.Playlists;
45import android.sax.Element;
46import android.sax.ElementListener;
47import android.sax.RootElement;
48import android.text.TextUtils;
49import android.util.Log;
50import android.util.Xml;
51
52import java.io.BufferedReader;
53import java.io.File;
54import java.io.FileDescriptor;
55import java.io.FileInputStream;
56import java.io.IOException;
57import java.io.InputStreamReader;
58import java.util.ArrayList;
59import java.util.HashMap;
60import java.util.HashSet;
61import java.util.Iterator;
62import java.util.Locale;
63
64/**
65 * Internal service helper that no-one should use directly.
66 *
67 * The way the scan currently works is:
68 * - The Java MediaScannerService creates a MediaScanner (this class), and calls
69 *   MediaScanner.scanDirectories on it.
70 * - scanDirectories() calls the native processDirectory() for each of the specified directories.
71 * - the processDirectory() JNI method wraps the provided mediascanner client in a native
72 *   'MyMediaScannerClient' class, then calls processDirectory() on the native MediaScanner
73 *   object (which got created when the Java MediaScanner was created).
74 * - native MediaScanner.processDirectory() (currently part of opencore) calls
75 *   doProcessDirectory(), which recurses over the folder, and calls
76 *   native MyMediaScannerClient.scanFile() for every file whose extension matches.
77 * - native MyMediaScannerClient.scanFile() calls back on Java MediaScannerClient.scanFile,
78 *   which calls doScanFile, which after some setup calls back down to native code, calling
79 *   MediaScanner.processFile().
80 * - MediaScanner.processFile() calls one of several methods, depending on the type of the
81 *   file: parseMP3, parseMP4, parseMidi, parseOgg or parseWMA.
82 * - each of these methods gets metadata key/value pairs from the file, and repeatedly
83 *   calls native MyMediaScannerClient.handleStringTag, which calls back up to its Java
84 *   counterparts in this file.
85 * - Java handleStringTag() gathers the key/value pairs that it's interested in.
86 * - once processFile returns and we're back in Java code in doScanFile(), it calls
87 *   Java MyMediaScannerClient.endFile(), which takes all the data that's been
88 *   gathered and inserts an entry in to the database.
89 *
90 * In summary:
91 * Java MediaScannerService calls
92 * Java MediaScanner scanDirectories, which calls
93 * Java MediaScanner processDirectory (native method), which calls
94 * native MediaScanner processDirectory, which calls
95 * native MyMediaScannerClient scanFile, which calls
96 * Java MyMediaScannerClient scanFile, which calls
97 * Java MediaScannerClient doScanFile, which calls
98 * Java MediaScanner processFile (native method), which calls
99 * native MediaScanner processFile, which calls
100 * native parseMP3, parseMP4, parseMidi, parseOgg or parseWMA, which calls
101 * native MyMediaScanner handleStringTag, which calls
102 * Java MyMediaScanner handleStringTag.
103 * Once MediaScanner processFile returns, an entry is inserted in to the database.
104 *
105 * The MediaScanner class is not thread-safe, so it should only be used in a single threaded manner.
106 *
107 * {@hide}
108 */
109public class MediaScanner
110{
111    static {
112        System.loadLibrary("media_jni");
113        native_init();
114    }
115
116    private final static String TAG = "MediaScanner";
117
118    private static final String[] FILES_PRESCAN_PROJECTION = new String[] {
119            Files.FileColumns._ID, // 0
120            Files.FileColumns.DATA, // 1
121            Files.FileColumns.FORMAT, // 2
122            Files.FileColumns.DATE_MODIFIED, // 3
123    };
124
125    private static final String[] ID_PROJECTION = new String[] {
126            Files.FileColumns._ID,
127    };
128
129    private static final int FILES_PRESCAN_ID_COLUMN_INDEX = 0;
130    private static final int FILES_PRESCAN_PATH_COLUMN_INDEX = 1;
131    private static final int FILES_PRESCAN_FORMAT_COLUMN_INDEX = 2;
132    private static final int FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX = 3;
133
134    private static final String[] PLAYLIST_MEMBERS_PROJECTION = new String[] {
135            Audio.Playlists.Members.PLAYLIST_ID, // 0
136     };
137
138    private static final int ID_PLAYLISTS_COLUMN_INDEX = 0;
139    private static final int PATH_PLAYLISTS_COLUMN_INDEX = 1;
140    private static final int DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX = 2;
141
142    private static final String RINGTONES_DIR = "/ringtones/";
143    private static final String NOTIFICATIONS_DIR = "/notifications/";
144    private static final String ALARMS_DIR = "/alarms/";
145    private static final String MUSIC_DIR = "/music/";
146    private static final String PODCAST_DIR = "/podcasts/";
147
148    private static final String[] ID3_GENRES = {
149        // ID3v1 Genres
150        "Blues",
151        "Classic Rock",
152        "Country",
153        "Dance",
154        "Disco",
155        "Funk",
156        "Grunge",
157        "Hip-Hop",
158        "Jazz",
159        "Metal",
160        "New Age",
161        "Oldies",
162        "Other",
163        "Pop",
164        "R&B",
165        "Rap",
166        "Reggae",
167        "Rock",
168        "Techno",
169        "Industrial",
170        "Alternative",
171        "Ska",
172        "Death Metal",
173        "Pranks",
174        "Soundtrack",
175        "Euro-Techno",
176        "Ambient",
177        "Trip-Hop",
178        "Vocal",
179        "Jazz+Funk",
180        "Fusion",
181        "Trance",
182        "Classical",
183        "Instrumental",
184        "Acid",
185        "House",
186        "Game",
187        "Sound Clip",
188        "Gospel",
189        "Noise",
190        "AlternRock",
191        "Bass",
192        "Soul",
193        "Punk",
194        "Space",
195        "Meditative",
196        "Instrumental Pop",
197        "Instrumental Rock",
198        "Ethnic",
199        "Gothic",
200        "Darkwave",
201        "Techno-Industrial",
202        "Electronic",
203        "Pop-Folk",
204        "Eurodance",
205        "Dream",
206        "Southern Rock",
207        "Comedy",
208        "Cult",
209        "Gangsta",
210        "Top 40",
211        "Christian Rap",
212        "Pop/Funk",
213        "Jungle",
214        "Native American",
215        "Cabaret",
216        "New Wave",
217        "Psychadelic",
218        "Rave",
219        "Showtunes",
220        "Trailer",
221        "Lo-Fi",
222        "Tribal",
223        "Acid Punk",
224        "Acid Jazz",
225        "Polka",
226        "Retro",
227        "Musical",
228        "Rock & Roll",
229        "Hard Rock",
230        // The following genres are Winamp extensions
231        "Folk",
232        "Folk-Rock",
233        "National Folk",
234        "Swing",
235        "Fast Fusion",
236        "Bebob",
237        "Latin",
238        "Revival",
239        "Celtic",
240        "Bluegrass",
241        "Avantgarde",
242        "Gothic Rock",
243        "Progressive Rock",
244        "Psychedelic Rock",
245        "Symphonic Rock",
246        "Slow Rock",
247        "Big Band",
248        "Chorus",
249        "Easy Listening",
250        "Acoustic",
251        "Humour",
252        "Speech",
253        "Chanson",
254        "Opera",
255        "Chamber Music",
256        "Sonata",
257        "Symphony",
258        "Booty Bass",
259        "Primus",
260        "Porn Groove",
261        "Satire",
262        "Slow Jam",
263        "Club",
264        "Tango",
265        "Samba",
266        "Folklore",
267        "Ballad",
268        "Power Ballad",
269        "Rhythmic Soul",
270        "Freestyle",
271        "Duet",
272        "Punk Rock",
273        "Drum Solo",
274        "A capella",
275        "Euro-House",
276        "Dance Hall",
277        // The following ones seem to be fairly widely supported as well
278        "Goa",
279        "Drum & Bass",
280        "Club-House",
281        "Hardcore",
282        "Terror",
283        "Indie",
284        "Britpop",
285        "Negerpunk",
286        "Polsk Punk",
287        "Beat",
288        "Christian Gangsta",
289        "Heavy Metal",
290        "Black Metal",
291        "Crossover",
292        "Contemporary Christian",
293        "Christian Rock",
294        "Merengue",
295        "Salsa",
296        "Thrash Metal",
297        "Anime",
298        "JPop",
299        "Synthpop",
300        // 148 and up don't seem to have been defined yet.
301    };
302
303    private int mNativeContext;
304    private Context mContext;
305    private IContentProvider mMediaProvider;
306    private Uri mAudioUri;
307    private Uri mVideoUri;
308    private Uri mImagesUri;
309    private Uri mThumbsUri;
310    private Uri mPlaylistsUri;
311    private Uri mFilesUri;
312    private boolean mProcessPlaylists, mProcessGenres;
313    private int mMtpObjectHandle;
314
315    private final String mExternalStoragePath;
316
317    /** whether to use bulk inserts or individual inserts for each item */
318    private static final boolean ENABLE_BULK_INSERTS = true;
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 final 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 final 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
950                int mediaType = 0;
951                if (!MediaScanner.isNoMediaPath(entry.mPath)) {
952                    int fileType = MediaFile.getFileTypeForMimeType(mMimeType);
953                    if (MediaFile.isAudioFileType(fileType)) {
954                        mediaType = FileColumns.MEDIA_TYPE_AUDIO;
955                    } else if (MediaFile.isVideoFileType(fileType)) {
956                        mediaType = FileColumns.MEDIA_TYPE_VIDEO;
957                    } else if (MediaFile.isImageFileType(fileType)) {
958                        mediaType = FileColumns.MEDIA_TYPE_IMAGE;
959                    } else if (MediaFile.isPlayListFileType(fileType)) {
960                        mediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
961                    }
962                    values.put(FileColumns.MEDIA_TYPE, mediaType);
963                }
964
965                mMediaProvider.update(result, values, null, null);
966            }
967
968            if(needToSetSettings) {
969                if (notifications) {
970                    setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
971                    mDefaultNotificationSet = true;
972                } else if (ringtones) {
973                    setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
974                    mDefaultRingtoneSet = true;
975                } else if (alarms) {
976                    setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
977                    mDefaultAlarmSet = true;
978                }
979            }
980
981            return result;
982        }
983
984        private boolean doesPathHaveFilename(String path, String filename) {
985            int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1;
986            int filenameLength = filename.length();
987            return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) &&
988                    pathFilenameStart + filenameLength == path.length();
989        }
990
991        private void setSettingIfNotSet(String settingName, Uri uri, long rowId) {
992
993            String existingSettingValue = Settings.System.getString(mContext.getContentResolver(),
994                    settingName);
995
996            if (TextUtils.isEmpty(existingSettingValue)) {
997                // Set the setting to the given URI
998                Settings.System.putString(mContext.getContentResolver(), settingName,
999                        ContentUris.withAppendedId(uri, rowId).toString());
1000            }
1001        }
1002
1003        private int getFileTypeFromDrm(String path) {
1004            if (!isDrmEnabled()) {
1005                return 0;
1006            }
1007
1008            int resultFileType = 0;
1009
1010            if (mDrmManagerClient == null) {
1011                mDrmManagerClient = new DrmManagerClient(mContext);
1012            }
1013
1014            if (mDrmManagerClient.canHandle(path, null)) {
1015                String drmMimetype = mDrmManagerClient.getOriginalMimeType(path);
1016                if (drmMimetype != null) {
1017                    mMimeType = drmMimetype;
1018                    resultFileType = MediaFile.getFileTypeForMimeType(drmMimetype);
1019                }
1020            }
1021            return resultFileType;
1022        }
1023
1024    }; // end of anonymous MediaScannerClient instance
1025
1026    private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
1027        Cursor c = null;
1028        String where = null;
1029        String[] selectionArgs = null;
1030
1031        if (mFileCache == null) {
1032            mFileCache = new HashMap<String, FileCacheEntry>();
1033        } else {
1034            mFileCache.clear();
1035        }
1036        if (mPlayLists == null) {
1037            mPlayLists = new ArrayList<FileCacheEntry>();
1038        } else {
1039            mPlayLists.clear();
1040        }
1041
1042        if (filePath != null) {
1043            // query for only one file
1044            where = Files.FileColumns.DATA + "=?";
1045            selectionArgs = new String[] { filePath };
1046        }
1047
1048        // Build the list of files from the content provider
1049        try {
1050            if (prescanFiles) {
1051                // First read existing files from the files table
1052
1053                c = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION,
1054                        where, selectionArgs, null, null);
1055
1056                if (c != null) {
1057                    mWasEmptyPriorToScan = c.getCount() == 0;
1058                    while (c.moveToNext()) {
1059                        long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
1060                        String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
1061                        int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
1062                        long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
1063
1064                        // Only consider entries with absolute path names.
1065                        // This allows storing URIs in the database without the
1066                        // media scanner removing them.
1067                        if (path != null && path.startsWith("/")) {
1068                            String key = path;
1069                            if (mCaseInsensitivePaths) {
1070                                key = path.toLowerCase();
1071                            }
1072
1073                            FileCacheEntry entry = new FileCacheEntry(rowId, path,
1074                                    lastModified, format);
1075                            mFileCache.put(key, entry);
1076                        }
1077                    }
1078                    c.close();
1079                    c = null;
1080                }
1081            }
1082        }
1083        finally {
1084            if (c != null) {
1085                c.close();
1086            }
1087        }
1088
1089        // compute original size of images
1090        mOriginalCount = 0;
1091        c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null, null);
1092        if (c != null) {
1093            mOriginalCount = c.getCount();
1094            c.close();
1095        }
1096    }
1097
1098    private boolean inScanDirectory(String path, String[] directories) {
1099        for (int i = 0; i < directories.length; i++) {
1100            String directory = directories[i];
1101            if (path.startsWith(directory)) {
1102                return true;
1103            }
1104        }
1105        return false;
1106    }
1107
1108    private void pruneDeadThumbnailFiles() {
1109        HashSet<String> existingFiles = new HashSet<String>();
1110        String directory = "/sdcard/DCIM/.thumbnails";
1111        String [] files = (new File(directory)).list();
1112        if (files == null)
1113            files = new String[0];
1114
1115        for (int i = 0; i < files.length; i++) {
1116            String fullPathString = directory + "/" + files[i];
1117            existingFiles.add(fullPathString);
1118        }
1119
1120        try {
1121            Cursor c = mMediaProvider.query(
1122                    mThumbsUri,
1123                    new String [] { "_data" },
1124                    null,
1125                    null,
1126                    null, null);
1127            Log.v(TAG, "pruneDeadThumbnailFiles... " + c);
1128            if (c != null && c.moveToFirst()) {
1129                do {
1130                    String fullPathString = c.getString(0);
1131                    existingFiles.remove(fullPathString);
1132                } while (c.moveToNext());
1133            }
1134
1135            for (String fileToDelete : existingFiles) {
1136                if (false)
1137                    Log.v(TAG, "fileToDelete is " + fileToDelete);
1138                try {
1139                    (new File(fileToDelete)).delete();
1140                } catch (SecurityException ex) {
1141                }
1142            }
1143
1144            Log.v(TAG, "/pruneDeadThumbnailFiles... " + c);
1145            if (c != null) {
1146                c.close();
1147            }
1148        } catch (RemoteException e) {
1149            // We will soon be killed...
1150        }
1151    }
1152
1153    static class MediaBulkDeleter {
1154        StringBuilder idList = new StringBuilder();
1155        IContentProvider mProvider;
1156        Uri mBaseUri;
1157
1158        public MediaBulkDeleter(IContentProvider provider, Uri baseUri) {
1159            mProvider = provider;
1160            mBaseUri = baseUri;
1161        }
1162
1163        public void delete(long id) throws RemoteException {
1164            if (idList.length() != 0) {
1165                idList.append(",");
1166            }
1167            idList.append(id);
1168            if (idList.length() > 1024) {
1169                flush();
1170            }
1171        }
1172        public void flush() throws RemoteException {
1173            int numrows = mProvider.delete(mBaseUri, MediaStore.MediaColumns._ID + " IN (" +
1174                    idList.toString() + ")", null);
1175            //Log.i("@@@@@@@@@", "rows deleted: " + numrows);
1176            idList.setLength(0);
1177        }
1178    }
1179
1180    private void postscan(String[] directories) throws RemoteException {
1181        Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();
1182
1183        // Tell the provider to not delete the file.
1184        // If the file is truly gone the delete is unnecessary, and we want to avoid
1185        // accidentally deleting files that are really there (this may happen if the
1186        // filesystem is mounted and unmounted while the scanner is running).
1187        Uri.Builder builder = mFilesUri.buildUpon();
1188        builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false");
1189        MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, builder.build());
1190
1191        while (iterator.hasNext()) {
1192            FileCacheEntry entry = iterator.next();
1193            String path = entry.mPath;
1194
1195            // remove database entries for files that no longer exist.
1196            boolean fileMissing = false;
1197
1198            if (!entry.mSeenInFileSystem && !MtpConstants.isAbstractObject(entry.mFormat)) {
1199                if (inScanDirectory(path, directories)) {
1200                    // we didn't see this file in the scan directory.
1201                    fileMissing = true;
1202                } else {
1203                    // the file actually a directory or other abstract object
1204                    // or is outside of our scan directory,
1205                    // so we need to check for file existence here.
1206                    File testFile = new File(path);
1207                    if (!testFile.exists()) {
1208                        fileMissing = true;
1209                    }
1210                }
1211            }
1212
1213            if (fileMissing) {
1214                // do not delete missing playlists, since they may have been modified by the user.
1215                // the user can delete them in the media player instead.
1216                // instead, clear the path and lastModified fields in the row
1217                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1218                int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1219
1220                if (!MediaFile.isPlayListFileType(fileType)) {
1221                    deleter.delete(entry.mRowId);
1222                    iterator.remove();
1223                    if (entry.mPath.toLowerCase(Locale.US).endsWith("/.nomedia")) {
1224                        deleter.flush();
1225                        File f = new File(path);
1226                        mMediaProvider.call(MediaStore.UNHIDE_CALL, f.getParent(), null);
1227                    }
1228                }
1229            }
1230        }
1231        deleter.flush();
1232
1233        // handle playlists last, after we know what media files are on the storage.
1234        if (mProcessPlaylists) {
1235            processPlayLists();
1236        }
1237
1238        if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
1239            pruneDeadThumbnailFiles();
1240
1241        // allow GC to clean up
1242        mPlayLists = null;
1243        mFileCache = null;
1244        mMediaProvider = null;
1245    }
1246
1247    private void initialize(String volumeName) {
1248        mMediaProvider = mContext.getContentResolver().acquireProvider("media");
1249
1250        mAudioUri = Audio.Media.getContentUri(volumeName);
1251        mVideoUri = Video.Media.getContentUri(volumeName);
1252        mImagesUri = Images.Media.getContentUri(volumeName);
1253        mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
1254        mFilesUri = Files.getContentUri(volumeName);
1255
1256        if (!volumeName.equals("internal")) {
1257            // we only support playlists on external media
1258            mProcessPlaylists = true;
1259            mProcessGenres = true;
1260            mPlaylistsUri = Playlists.getContentUri(volumeName);
1261
1262            mCaseInsensitivePaths = true;
1263        }
1264    }
1265
1266    public void scanDirectories(String[] directories, String volumeName) {
1267        try {
1268            long start = System.currentTimeMillis();
1269            initialize(volumeName);
1270            prescan(null, true);
1271            long prescan = System.currentTimeMillis();
1272
1273            if (ENABLE_BULK_INSERTS) {
1274                // create MediaInserter for bulk inserts
1275                mMediaInserter = new MediaInserter(mMediaProvider, 500);
1276            }
1277
1278            for (int i = 0; i < directories.length; i++) {
1279                processDirectory(directories[i], mClient);
1280            }
1281
1282            if (ENABLE_BULK_INSERTS) {
1283                // flush remaining inserts
1284                mMediaInserter.flushAll();
1285                mMediaInserter = null;
1286            }
1287
1288            long scan = System.currentTimeMillis();
1289            postscan(directories);
1290            long end = System.currentTimeMillis();
1291
1292            if (false) {
1293                Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n");
1294                Log.d(TAG, "    scan time: " + (scan - prescan) + "ms\n");
1295                Log.d(TAG, "postscan time: " + (end - scan) + "ms\n");
1296                Log.d(TAG, "   total time: " + (end - start) + "ms\n");
1297            }
1298        } catch (SQLException e) {
1299            // this might happen if the SD card is removed while the media scanner is running
1300            Log.e(TAG, "SQLException in MediaScanner.scan()", e);
1301        } catch (UnsupportedOperationException e) {
1302            // this might happen if the SD card is removed while the media scanner is running
1303            Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e);
1304        } catch (RemoteException e) {
1305            Log.e(TAG, "RemoteException in MediaScanner.scan()", e);
1306        }
1307    }
1308
1309    // this function is used to scan a single file
1310    public Uri scanSingleFile(String path, String volumeName, String mimeType) {
1311        try {
1312            initialize(volumeName);
1313            prescan(path, true);
1314
1315            File file = new File(path);
1316
1317            // lastModified is in milliseconds on Files.
1318            long lastModifiedSeconds = file.lastModified() / 1000;
1319
1320            // always scan the file, so we can return the content://media Uri for existing files
1321            return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(),
1322                    false, true, false);
1323        } catch (RemoteException e) {
1324            Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1325            return null;
1326        }
1327    }
1328
1329    private static boolean isNoMediaFile(String path) {
1330        File file = new File(path);
1331        if (file.isDirectory()) return false;
1332
1333        // special case certain file names
1334        // I use regionMatches() instead of substring() below
1335        // to avoid memory allocation
1336        int lastSlash = path.lastIndexOf('/');
1337        if (lastSlash >= 0 && lastSlash + 2 < path.length()) {
1338            // ignore those ._* files created by MacOS
1339            if (path.regionMatches(lastSlash + 1, "._", 0, 2)) {
1340                return true;
1341            }
1342
1343            // ignore album art files created by Windows Media Player:
1344            // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg
1345            // and AlbumArt_{...}_Small.jpg
1346            if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) {
1347                if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) ||
1348                        path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) {
1349                    return true;
1350                }
1351                int length = path.length() - lastSlash - 1;
1352                if ((length == 17 && path.regionMatches(
1353                        true, lastSlash + 1, "AlbumArtSmall", 0, 13)) ||
1354                        (length == 10
1355                         && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) {
1356                    return true;
1357                }
1358            }
1359        }
1360        return false;
1361    }
1362
1363    public static boolean isNoMediaPath(String path) {
1364        if (path == null) return false;
1365
1366        // return true if file or any parent directory has name starting with a dot
1367        if (path.indexOf("/.") >= 0) return true;
1368
1369        // now check to see if any parent directories have a ".nomedia" file
1370        // start from 1 so we don't bother checking in the root directory
1371        int offset = 1;
1372        while (offset >= 0) {
1373            int slashIndex = path.indexOf('/', offset);
1374            if (slashIndex > offset) {
1375                slashIndex++; // move past slash
1376                File file = new File(path.substring(0, slashIndex) + ".nomedia");
1377                if (file.exists()) {
1378                    // we have a .nomedia in one of the parent directories
1379                    return true;
1380                }
1381            }
1382            offset = slashIndex;
1383        }
1384        return isNoMediaFile(path);
1385    }
1386
1387    public void scanMtpFile(String path, String volumeName, int objectHandle, int format) {
1388        initialize(volumeName);
1389        MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1390        int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1391        File file = new File(path);
1392        long lastModifiedSeconds = file.lastModified() / 1000;
1393
1394        if (!MediaFile.isAudioFileType(fileType) && !MediaFile.isVideoFileType(fileType) &&
1395            !MediaFile.isImageFileType(fileType) && !MediaFile.isPlayListFileType(fileType)) {
1396
1397            // no need to use the media scanner, but we need to update last modified and file size
1398            ContentValues values = new ContentValues();
1399            values.put(Files.FileColumns.SIZE, file.length());
1400            values.put(Files.FileColumns.DATE_MODIFIED, lastModifiedSeconds);
1401            try {
1402                String[] whereArgs = new String[] {  Integer.toString(objectHandle) };
1403                mMediaProvider.update(Files.getMtpObjectsUri(volumeName), values, "_id=?",
1404                        whereArgs);
1405            } catch (RemoteException e) {
1406                Log.e(TAG, "RemoteException in scanMtpFile", e);
1407            }
1408            return;
1409        }
1410
1411        mMtpObjectHandle = objectHandle;
1412        try {
1413            if (MediaFile.isPlayListFileType(fileType)) {
1414                // build file cache so we can look up tracks in the playlist
1415                prescan(null, true);
1416
1417                String key = path;
1418                if (mCaseInsensitivePaths) {
1419                    key = path.toLowerCase();
1420                }
1421                FileCacheEntry entry = mFileCache.get(key);
1422                if (entry != null) {
1423                    processPlayList(entry);
1424                }
1425            } else {
1426                // MTP will create a file entry for us so we don't want to do it in prescan
1427                prescan(path, false);
1428
1429                // always scan the file, so we can return the content://media Uri for existing files
1430                mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(),
1431                    (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path));
1432            }
1433        } catch (RemoteException e) {
1434            Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1435        } finally {
1436            mMtpObjectHandle = 0;
1437        }
1438    }
1439
1440    // returns the number of matching file/directory names, starting from the right
1441    private int matchPaths(String path1, String path2) {
1442        int result = 0;
1443        int end1 = path1.length();
1444        int end2 = path2.length();
1445
1446        while (end1 > 0 && end2 > 0) {
1447            int slash1 = path1.lastIndexOf('/', end1 - 1);
1448            int slash2 = path2.lastIndexOf('/', end2 - 1);
1449            int backSlash1 = path1.lastIndexOf('\\', end1 - 1);
1450            int backSlash2 = path2.lastIndexOf('\\', end2 - 1);
1451            int start1 = (slash1 > backSlash1 ? slash1 : backSlash1);
1452            int start2 = (slash2 > backSlash2 ? slash2 : backSlash2);
1453            if (start1 < 0) start1 = 0; else start1++;
1454            if (start2 < 0) start2 = 0; else start2++;
1455            int length = end1 - start1;
1456            if (end2 - start2 != length) break;
1457            if (path1.regionMatches(true, start1, path2, start2, length)) {
1458                result++;
1459                end1 = start1 - 1;
1460                end2 = start2 - 1;
1461            } else break;
1462        }
1463
1464        return result;
1465    }
1466
1467    private boolean addPlayListEntry(String entry, String playListDirectory,
1468            Uri uri, ContentValues values, int index) {
1469
1470        // watch for trailing whitespace
1471        int entryLength = entry.length();
1472        while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--;
1473        // path should be longer than 3 characters.
1474        // avoid index out of bounds errors below by returning here.
1475        if (entryLength < 3) return false;
1476        if (entryLength < entry.length()) entry = entry.substring(0, entryLength);
1477
1478        // does entry appear to be an absolute path?
1479        // look for Unix or DOS absolute paths
1480        char ch1 = entry.charAt(0);
1481        boolean fullPath = (ch1 == '/' ||
1482                (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\'));
1483        // if we have a relative path, combine entry with playListDirectory
1484        if (!fullPath)
1485            entry = playListDirectory + entry;
1486
1487        //FIXME - should we look for "../" within the path?
1488
1489        // best matching MediaFile for the play list entry
1490        FileCacheEntry bestMatch = null;
1491
1492        // number of rightmost file/directory names for bestMatch
1493        int bestMatchLength = 0;
1494
1495        Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();
1496        while (iterator.hasNext()) {
1497            FileCacheEntry cacheEntry = iterator.next();
1498            String path = cacheEntry.mPath;
1499
1500            if (path.equalsIgnoreCase(entry)) {
1501                bestMatch = cacheEntry;
1502                break;    // don't bother continuing search
1503            }
1504
1505            int matchLength = matchPaths(path, entry);
1506            if (matchLength > bestMatchLength) {
1507                bestMatch = cacheEntry;
1508                bestMatchLength = matchLength;
1509            }
1510        }
1511
1512        if (bestMatch == null) {
1513            return false;
1514        }
1515
1516        try {
1517            // check rowid is set. Rowid may be missing if it is inserted by bulkInsert().
1518            if (bestMatch.mRowId == 0) {
1519                Cursor c = mMediaProvider.query(mAudioUri, ID_PROJECTION,
1520                        MediaStore.Files.FileColumns.DATA + "=?",
1521                        new String[] { bestMatch.mPath }, null, null);
1522                if (c != null) {
1523                    if (c.moveToNext()) {
1524                        bestMatch.mRowId = c.getLong(0);
1525                    }
1526                    c.close();
1527                }
1528                if (bestMatch.mRowId == 0) {
1529                    return false;
1530                }
1531            }
1532            // OK, now we are ready to add this to the database
1533            values.clear();
1534            values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index));
1535            values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId));
1536            mMediaProvider.insert(uri, values);
1537        } catch (RemoteException e) {
1538            Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e);
1539            return false;
1540        }
1541
1542        return true;
1543    }
1544
1545    private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) {
1546        BufferedReader reader = null;
1547        try {
1548            File f = new File(path);
1549            if (f.exists()) {
1550                reader = new BufferedReader(
1551                        new InputStreamReader(new FileInputStream(f)), 8192);
1552                String line = reader.readLine();
1553                int index = 0;
1554                while (line != null) {
1555                    // ignore comment lines, which begin with '#'
1556                    if (line.length() > 0 && line.charAt(0) != '#') {
1557                        values.clear();
1558                        if (addPlayListEntry(line, playListDirectory, uri, values, index))
1559                            index++;
1560                    }
1561                    line = reader.readLine();
1562                }
1563            }
1564        } catch (IOException e) {
1565            Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1566        } finally {
1567            try {
1568                if (reader != null)
1569                    reader.close();
1570            } catch (IOException e) {
1571                Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1572            }
1573        }
1574    }
1575
1576    private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) {
1577        BufferedReader reader = null;
1578        try {
1579            File f = new File(path);
1580            if (f.exists()) {
1581                reader = new BufferedReader(
1582                        new InputStreamReader(new FileInputStream(f)), 8192);
1583                String line = reader.readLine();
1584                int index = 0;
1585                while (line != null) {
1586                    // ignore comment lines, which begin with '#'
1587                    if (line.startsWith("File")) {
1588                        int equals = line.indexOf('=');
1589                        if (equals > 0) {
1590                            values.clear();
1591                            if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index))
1592                                index++;
1593                        }
1594                    }
1595                    line = reader.readLine();
1596                }
1597            }
1598        } catch (IOException e) {
1599            Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1600        } finally {
1601            try {
1602                if (reader != null)
1603                    reader.close();
1604            } catch (IOException e) {
1605                Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1606            }
1607        }
1608    }
1609
1610    class WplHandler implements ElementListener {
1611
1612        final ContentHandler handler;
1613        String playListDirectory;
1614        Uri uri;
1615        ContentValues values = new ContentValues();
1616        int index = 0;
1617
1618        public WplHandler(String playListDirectory, Uri uri) {
1619            this.playListDirectory = playListDirectory;
1620            this.uri = uri;
1621
1622            RootElement root = new RootElement("smil");
1623            Element body = root.getChild("body");
1624            Element seq = body.getChild("seq");
1625            Element media = seq.getChild("media");
1626            media.setElementListener(this);
1627
1628            this.handler = root.getContentHandler();
1629        }
1630
1631        public void start(Attributes attributes) {
1632            String path = attributes.getValue("", "src");
1633            if (path != null) {
1634                values.clear();
1635                if (addPlayListEntry(path, playListDirectory, uri, values, index)) {
1636                    index++;
1637                }
1638            }
1639        }
1640
1641       public void end() {
1642       }
1643
1644        ContentHandler getContentHandler() {
1645            return handler;
1646        }
1647    }
1648
1649    private void processWplPlayList(String path, String playListDirectory, Uri uri) {
1650        FileInputStream fis = null;
1651        try {
1652            File f = new File(path);
1653            if (f.exists()) {
1654                fis = new FileInputStream(f);
1655
1656                Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler());
1657            }
1658        } catch (SAXException e) {
1659            e.printStackTrace();
1660        } catch (IOException e) {
1661            e.printStackTrace();
1662        } finally {
1663            try {
1664                if (fis != null)
1665                    fis.close();
1666            } catch (IOException e) {
1667                Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e);
1668            }
1669        }
1670    }
1671
1672    private void processPlayList(FileCacheEntry entry) throws RemoteException {
1673        String path = entry.mPath;
1674        ContentValues values = new ContentValues();
1675        int lastSlash = path.lastIndexOf('/');
1676        if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path);
1677        Uri uri, membersUri;
1678        long rowId = entry.mRowId;
1679
1680        // make sure we have a name
1681        String name = values.getAsString(MediaStore.Audio.Playlists.NAME);
1682        if (name == null) {
1683            name = values.getAsString(MediaStore.MediaColumns.TITLE);
1684            if (name == null) {
1685                // extract name from file name
1686                int lastDot = path.lastIndexOf('.');
1687                name = (lastDot < 0 ? path.substring(lastSlash + 1)
1688                        : path.substring(lastSlash + 1, lastDot));
1689            }
1690        }
1691
1692        values.put(MediaStore.Audio.Playlists.NAME, name);
1693        values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1694
1695        if (rowId == 0) {
1696            values.put(MediaStore.Audio.Playlists.DATA, path);
1697            uri = mMediaProvider.insert(mPlaylistsUri, values);
1698            rowId = ContentUris.parseId(uri);
1699            membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1700        } else {
1701            uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
1702            mMediaProvider.update(uri, values, null, null);
1703
1704            // delete members of existing playlist
1705            membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1706            mMediaProvider.delete(membersUri, null, null);
1707        }
1708
1709        String playListDirectory = path.substring(0, lastSlash + 1);
1710        MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1711        int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1712
1713        if (fileType == MediaFile.FILE_TYPE_M3U) {
1714            processM3uPlayList(path, playListDirectory, membersUri, values);
1715        } else if (fileType == MediaFile.FILE_TYPE_PLS) {
1716            processPlsPlayList(path, playListDirectory, membersUri, values);
1717        } else if (fileType == MediaFile.FILE_TYPE_WPL) {
1718            processWplPlayList(path, playListDirectory, membersUri);
1719        }
1720    }
1721
1722    private void processPlayLists() throws RemoteException {
1723        Iterator<FileCacheEntry> iterator = mPlayLists.iterator();
1724        while (iterator.hasNext()) {
1725            FileCacheEntry entry = iterator.next();
1726            // only process playlist files if they are new or have been modified since the last scan
1727            if (entry.mLastModifiedChanged) {
1728                processPlayList(entry);
1729            }
1730        }
1731    }
1732
1733    private native void processDirectory(String path, MediaScannerClient client);
1734    private native void processFile(String path, String mimeType, MediaScannerClient client);
1735    public native void setLocale(String locale);
1736
1737    public native byte[] extractAlbumArt(FileDescriptor fd);
1738
1739    private static native final void native_init();
1740    private native final void native_setup();
1741    private native final void native_finalize();
1742
1743    /**
1744     * Releases resouces associated with this MediaScanner object.
1745     * It is considered good practice to call this method when
1746     * one is done using the MediaScanner object. After this method
1747     * is called, the MediaScanner object can no longer be used.
1748     */
1749    public void release() {
1750        native_finalize();
1751    }
1752
1753    @Override
1754    protected void finalize() {
1755        mContext.getContentResolver().releaseProvider(mMediaProvider);
1756        native_finalize();
1757    }
1758}
1759