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