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