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