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