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