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