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