Home | History | Annotate | Download | only in media
      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.text.TextUtils;
     48 import android.util.Log;
     49 import android.util.Xml;
     50 
     51 import java.io.BufferedReader;
     52 import java.io.File;
     53 import java.io.FileDescriptor;
     54 import java.io.FileInputStream;
     55 import java.io.IOException;
     56 import java.io.InputStreamReader;
     57 import java.util.ArrayList;
     58 import java.util.HashSet;
     59 import java.util.Iterator;
     60 import java.util.Locale;
     61 
     62 import libcore.io.ErrnoException;
     63 import libcore.io.Libcore;
     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");
    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 int 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 
    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 ArrayList<PlaylistEntry> mPlaylistEntries = new ArrayList<PlaylistEntry>();
    382 
    383     private MediaInserter mMediaInserter;
    384 
    385     private ArrayList<FileEntry> mPlayLists;
    386 
    387     private DrmManagerClient mDrmManagerClient = null;
    388 
    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 
    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 
    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 
    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                             if (mExternalIsEmulated && path.startsWith(mExternalStoragePath)) {
    557                                 // try to rewrite the path to bypass the sd card fuse layer
    558                                 String directPath = Environment.getMediaStorageDirectory() +
    559                                         path.substring(mExternalStoragePath.length());
    560                                 File f = new File(directPath);
    561                                 if (f.exists()) {
    562                                     path = directPath;
    563                                 }
    564                             }
    565                         }
    566 
    567                         // we only extract metadata for audio and video files
    568                         if (isaudio || isvideo) {
    569                             processFile(path, mimeType, this);
    570                         }
    571 
    572                         if (isimage) {
    573                             processImageFile(path);
    574                         }
    575 
    576                         result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
    577                     }
    578                 }
    579             } catch (RemoteException e) {
    580                 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
    581             }
    582 //            long t2 = System.currentTimeMillis();
    583 //            Log.v(TAG, "scanFile: " + path + " took " + (t2-t1));
    584             return result;
    585         }
    586 
    587         private int parseSubstring(String s, int start, int defaultValue) {
    588             int length = s.length();
    589             if (start == length) return defaultValue;
    590 
    591             char ch = s.charAt(start++);
    592             // return defaultValue if we have no integer at all
    593             if (ch < '0' || ch > '9') return defaultValue;
    594 
    595             int result = ch - '0';
    596             while (start < length) {
    597                 ch = s.charAt(start++);
    598                 if (ch < '0' || ch > '9') return result;
    599                 result = result * 10 + (ch - '0');
    600             }
    601 
    602             return result;
    603         }
    604 
    605         public void handleStringTag(String name, String value) {
    606             if (name.equalsIgnoreCase("title") || name.startsWith("title;")) {
    607                 // Don't trim() here, to preserve the special \001 character
    608                 // used to force sorting. The media provider will trim() before
    609                 // inserting the title in to the database.
    610                 mTitle = value;
    611             } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) {
    612                 mArtist = value.trim();
    613             } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")
    614                     || name.equalsIgnoreCase("band") || name.startsWith("band;")) {
    615                 mAlbumArtist = value.trim();
    616             } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) {
    617                 mAlbum = value.trim();
    618             } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) {
    619                 mComposer = value.trim();
    620             } else if (mProcessGenres &&
    621                     (name.equalsIgnoreCase("genre") || name.startsWith("genre;"))) {
    622                 mGenre = getGenreName(value);
    623             } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) {
    624                 mYear = parseSubstring(value, 0, 0);
    625             } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) {
    626                 // track number might be of the form "2/12"
    627                 // we just read the number before the slash
    628                 int num = parseSubstring(value, 0, 0);
    629                 mTrack = (mTrack / 1000) * 1000 + num;
    630             } else if (name.equalsIgnoreCase("discnumber") ||
    631                     name.equals("set") || name.startsWith("set;")) {
    632                 // set number might be of the form "1/3"
    633                 // we just read the number before the slash
    634                 int num = parseSubstring(value, 0, 0);
    635                 mTrack = (num * 1000) + (mTrack % 1000);
    636             } else if (name.equalsIgnoreCase("duration")) {
    637                 mDuration = parseSubstring(value, 0, 0);
    638             } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {
    639                 mWriter = value.trim();
    640             } else if (name.equalsIgnoreCase("compilation")) {
    641                 mCompilation = parseSubstring(value, 0, 0);
    642             } else if (name.equalsIgnoreCase("isdrm")) {
    643                 mIsDrm = (parseSubstring(value, 0, 0) == 1);
    644             } else if (name.equalsIgnoreCase("width")) {
    645                 mWidth = parseSubstring(value, 0, 0);
    646             } else if (name.equalsIgnoreCase("height")) {
    647                 mHeight = parseSubstring(value, 0, 0);
    648             } else {
    649                 //Log.v(TAG, "unknown tag: " + name + " (" + mProcessGenres + ")");
    650             }
    651         }
    652 
    653         private boolean convertGenreCode(String input, String expected) {
    654             String output = getGenreName(input);
    655             if (output.equals(expected)) {
    656                 return true;
    657             } else {
    658                 Log.d(TAG, "'" + input + "' -> '" + output + "', expected '" + expected + "'");
    659                 return false;
    660             }
    661         }
    662         private void testGenreNameConverter() {
    663             convertGenreCode("2", "Country");
    664             convertGenreCode("(2)", "Country");
    665             convertGenreCode("(2", "(2");
    666             convertGenreCode("2 Foo", "Country");
    667             convertGenreCode("(2) Foo", "Country");
    668             convertGenreCode("(2 Foo", "(2 Foo");
    669             convertGenreCode("2Foo", "2Foo");
    670             convertGenreCode("(2)Foo", "Country");
    671             convertGenreCode("200 Foo", "Foo");
    672             convertGenreCode("(200) Foo", "Foo");
    673             convertGenreCode("200Foo", "200Foo");
    674             convertGenreCode("(200)Foo", "Foo");
    675             convertGenreCode("200)Foo", "200)Foo");
    676             convertGenreCode("200) Foo", "200) Foo");
    677         }
    678 
    679         public String getGenreName(String genreTagValue) {
    680 
    681             if (genreTagValue == null) {
    682                 return null;
    683             }
    684             final int length = genreTagValue.length();
    685 
    686             if (length > 0) {
    687                 boolean parenthesized = false;
    688                 StringBuffer number = new StringBuffer();
    689                 int i = 0;
    690                 for (; i < length; ++i) {
    691                     char c = genreTagValue.charAt(i);
    692                     if (i == 0 && c == '(') {
    693                         parenthesized = true;
    694                     } else if (Character.isDigit(c)) {
    695                         number.append(c);
    696                     } else {
    697                         break;
    698                     }
    699                 }
    700                 char charAfterNumber = i < length ? genreTagValue.charAt(i) : ' ';
    701                 if ((parenthesized && charAfterNumber == ')')
    702                         || !parenthesized && Character.isWhitespace(charAfterNumber)) {
    703                     try {
    704                         short genreIndex = Short.parseShort(number.toString());
    705                         if (genreIndex >= 0) {
    706                             if (genreIndex < ID3_GENRES.length && ID3_GENRES[genreIndex] != null) {
    707                                 return ID3_GENRES[genreIndex];
    708                             } else if (genreIndex == 0xFF) {
    709                                 return null;
    710                             } else if (genreIndex < 0xFF && (i + 1) < length) {
    711                                 // genre is valid but unknown,
    712                                 // if there is a string after the value we take it
    713                                 if (parenthesized && charAfterNumber == ')') {
    714                                     i++;
    715                                 }
    716                                 String ret = genreTagValue.substring(i).trim();
    717                                 if (ret.length() != 0) {
    718                                     return ret;
    719                                 }
    720                             } else {
    721                                 // else return the number, without parentheses
    722                                 return number.toString();
    723                             }
    724                         }
    725                     } catch (NumberFormatException e) {
    726                     }
    727                 }
    728             }
    729 
    730             return genreTagValue;
    731         }
    732 
    733         private void processImageFile(String path) {
    734             try {
    735                 mBitmapOptions.outWidth = 0;
    736                 mBitmapOptions.outHeight = 0;
    737                 BitmapFactory.decodeFile(path, mBitmapOptions);
    738                 mWidth = mBitmapOptions.outWidth;
    739                 mHeight = mBitmapOptions.outHeight;
    740             } catch (Throwable th) {
    741                 // ignore;
    742             }
    743         }
    744 
    745         public void setMimeType(String mimeType) {
    746             if ("audio/mp4".equals(mMimeType) &&
    747                     mimeType.startsWith("video")) {
    748                 // for feature parity with Donut, we force m4a files to keep the
    749                 // audio/mp4 mimetype, even if they are really "enhanced podcasts"
    750                 // with a video track
    751                 return;
    752             }
    753             mMimeType = mimeType;
    754             mFileType = MediaFile.getFileTypeForMimeType(mimeType);
    755         }
    756 
    757         /**
    758          * Formats the data into a values array suitable for use with the Media
    759          * Content Provider.
    760          *
    761          * @return a map of values
    762          */
    763         private ContentValues toValues() {
    764             ContentValues map = new ContentValues();
    765 
    766             map.put(MediaStore.MediaColumns.DATA, mPath);
    767             map.put(MediaStore.MediaColumns.TITLE, mTitle);
    768             map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified);
    769             map.put(MediaStore.MediaColumns.SIZE, mFileSize);
    770             map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType);
    771             map.put(MediaStore.MediaColumns.IS_DRM, mIsDrm);
    772 
    773             String resolution = null;
    774             if (mWidth > 0 && mHeight > 0) {
    775                 map.put(MediaStore.MediaColumns.WIDTH, mWidth);
    776                 map.put(MediaStore.MediaColumns.HEIGHT, mHeight);
    777                 resolution = mWidth + "x" + mHeight;
    778             }
    779 
    780             if (!mNoMedia) {
    781                 if (MediaFile.isVideoFileType(mFileType)) {
    782                     map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0
    783                             ? mArtist : MediaStore.UNKNOWN_STRING));
    784                     map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0
    785                             ? mAlbum : MediaStore.UNKNOWN_STRING));
    786                     map.put(Video.Media.DURATION, mDuration);
    787                     if (resolution != null) {
    788                         map.put(Video.Media.RESOLUTION, resolution);
    789                     }
    790                 } else if (MediaFile.isImageFileType(mFileType)) {
    791                     // FIXME - add DESCRIPTION
    792                 } else if (MediaFile.isAudioFileType(mFileType)) {
    793                     map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ?
    794                             mArtist : MediaStore.UNKNOWN_STRING);
    795                     map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null &&
    796                             mAlbumArtist.length() > 0) ? mAlbumArtist : null);
    797                     map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ?
    798                             mAlbum : MediaStore.UNKNOWN_STRING);
    799                     map.put(Audio.Media.COMPOSER, mComposer);
    800                     map.put(Audio.Media.GENRE, mGenre);
    801                     if (mYear != 0) {
    802                         map.put(Audio.Media.YEAR, mYear);
    803                     }
    804                     map.put(Audio.Media.TRACK, mTrack);
    805                     map.put(Audio.Media.DURATION, mDuration);
    806                     map.put(Audio.Media.COMPILATION, mCompilation);
    807                 }
    808             }
    809             return map;
    810         }
    811 
    812         private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
    813                 boolean alarms, boolean music, boolean podcasts)
    814                 throws RemoteException {
    815             // update database
    816 
    817             // use album artist if artist is missing
    818             if (mArtist == null || mArtist.length() == 0) {
    819                 mArtist = mAlbumArtist;
    820             }
    821 
    822             ContentValues values = toValues();
    823             String title = values.getAsString(MediaStore.MediaColumns.TITLE);
    824             if (title == null || TextUtils.isEmpty(title.trim())) {
    825                 title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA));
    826                 values.put(MediaStore.MediaColumns.TITLE, title);
    827             }
    828             String album = values.getAsString(Audio.Media.ALBUM);
    829             if (MediaStore.UNKNOWN_STRING.equals(album)) {
    830                 album = values.getAsString(MediaStore.MediaColumns.DATA);
    831                 // extract last path segment before file name
    832                 int lastSlash = album.lastIndexOf('/');
    833                 if (lastSlash >= 0) {
    834                     int previousSlash = 0;
    835                     while (true) {
    836                         int idx = album.indexOf('/', previousSlash + 1);
    837                         if (idx < 0 || idx >= lastSlash) {
    838                             break;
    839                         }
    840                         previousSlash = idx;
    841                     }
    842                     if (previousSlash != 0) {
    843                         album = album.substring(previousSlash + 1, lastSlash);
    844                         values.put(Audio.Media.ALBUM, album);
    845                     }
    846                 }
    847             }
    848             long rowId = entry.mRowId;
    849             if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) {
    850                 // Only set these for new entries. For existing entries, they
    851                 // may have been modified later, and we want to keep the current
    852                 // values so that custom ringtones still show up in the ringtone
    853                 // picker.
    854                 values.put(Audio.Media.IS_RINGTONE, ringtones);
    855                 values.put(Audio.Media.IS_NOTIFICATION, notifications);
    856                 values.put(Audio.Media.IS_ALARM, alarms);
    857                 values.put(Audio.Media.IS_MUSIC, music);
    858                 values.put(Audio.Media.IS_PODCAST, podcasts);
    859             } else if (mFileType == MediaFile.FILE_TYPE_JPEG && !mNoMedia) {
    860                 ExifInterface exif = null;
    861                 try {
    862                     exif = new ExifInterface(entry.mPath);
    863                 } catch (IOException ex) {
    864                     // exif is null
    865                 }
    866                 if (exif != null) {
    867                     float[] latlng = new float[2];
    868                     if (exif.getLatLong(latlng)) {
    869                         values.put(Images.Media.LATITUDE, latlng[0]);
    870                         values.put(Images.Media.LONGITUDE, latlng[1]);
    871                     }
    872 
    873                     long time = exif.getGpsDateTime();
    874                     if (time != -1) {
    875                         values.put(Images.Media.DATE_TAKEN, time);
    876                     } else {
    877                         // If no time zone information is available, we should consider using
    878                         // EXIF local time as taken time if the difference between file time
    879                         // and EXIF local time is not less than 1 Day, otherwise MediaProvider
    880                         // will use file time as taken time.
    881                         time = exif.getDateTime();
    882                         if (time != -1 && Math.abs(mLastModified * 1000 - time) >= 86400000) {
    883                             values.put(Images.Media.DATE_TAKEN, time);
    884                         }
    885                     }
    886 
    887                     int orientation = exif.getAttributeInt(
    888                         ExifInterface.TAG_ORIENTATION, -1);
    889                     if (orientation != -1) {
    890                         // We only recognize a subset of orientation tag values.
    891                         int degree;
    892                         switch(orientation) {
    893                             case ExifInterface.ORIENTATION_ROTATE_90:
    894                                 degree = 90;
    895                                 break;
    896                             case ExifInterface.ORIENTATION_ROTATE_180:
    897                                 degree = 180;
    898                                 break;
    899                             case ExifInterface.ORIENTATION_ROTATE_270:
    900                                 degree = 270;
    901                                 break;
    902                             default:
    903                                 degree = 0;
    904                                 break;
    905                         }
    906                         values.put(Images.Media.ORIENTATION, degree);
    907                     }
    908                 }
    909             }
    910 
    911             Uri tableUri = mFilesUri;
    912             MediaInserter inserter = mMediaInserter;
    913             if (!mNoMedia) {
    914                 if (MediaFile.isVideoFileType(mFileType)) {
    915                     tableUri = mVideoUri;
    916                 } else if (MediaFile.isImageFileType(mFileType)) {
    917                     tableUri = mImagesUri;
    918                 } else if (MediaFile.isAudioFileType(mFileType)) {
    919                     tableUri = mAudioUri;
    920                 }
    921             }
    922             Uri result = null;
    923             boolean needToSetSettings = false;
    924             if (rowId == 0) {
    925                 if (mMtpObjectHandle != 0) {
    926                     values.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle);
    927                 }
    928                 if (tableUri == mFilesUri) {
    929                     int format = entry.mFormat;
    930                     if (format == 0) {
    931                         format = MediaFile.getFormatCode(entry.mPath, mMimeType);
    932                     }
    933                     values.put(Files.FileColumns.FORMAT, format);
    934                 }
    935                 // Setting a flag in order not to use bulk insert for the file related with
    936                 // notifications, ringtones, and alarms, because the rowId of the inserted file is
    937                 // needed.
    938                 if (mWasEmptyPriorToScan) {
    939                     if (notifications && !mDefaultNotificationSet) {
    940                         if (TextUtils.isEmpty(mDefaultNotificationFilename) ||
    941                                 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) {
    942                             needToSetSettings = true;
    943                         }
    944                     } else if (ringtones && !mDefaultRingtoneSet) {
    945                         if (TextUtils.isEmpty(mDefaultRingtoneFilename) ||
    946                                 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) {
    947                             needToSetSettings = true;
    948                         }
    949                     } else if (alarms && !mDefaultAlarmSet) {
    950                         if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) ||
    951                                 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) {
    952                             needToSetSettings = true;
    953                         }
    954                     }
    955                 }
    956 
    957                 // New file, insert it.
    958                 // Directories need to be inserted before the files they contain, so they
    959                 // get priority when bulk inserting.
    960                 // If the rowId of the inserted file is needed, it gets inserted immediately,
    961                 // bypassing the bulk inserter.
    962                 if (inserter == null || needToSetSettings) {
    963                     if (inserter != null) {
    964                         inserter.flushAll();
    965                     }
    966                     result = mMediaProvider.insert(mPackageName, tableUri, values);
    967                 } else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) {
    968                     inserter.insertwithPriority(tableUri, values);
    969                 } else {
    970                     inserter.insert(tableUri, values);
    971                 }
    972 
    973                 if (result != null) {
    974                     rowId = ContentUris.parseId(result);
    975                     entry.mRowId = rowId;
    976                 }
    977             } else {
    978                 // updated file
    979                 result = ContentUris.withAppendedId(tableUri, rowId);
    980                 // path should never change, and we want to avoid replacing mixed cased paths
    981                 // with squashed lower case paths
    982                 values.remove(MediaStore.MediaColumns.DATA);
    983 
    984                 int mediaType = 0;
    985                 if (!MediaScanner.isNoMediaPath(entry.mPath)) {
    986                     int fileType = MediaFile.getFileTypeForMimeType(mMimeType);
    987                     if (MediaFile.isAudioFileType(fileType)) {
    988                         mediaType = FileColumns.MEDIA_TYPE_AUDIO;
    989                     } else if (MediaFile.isVideoFileType(fileType)) {
    990                         mediaType = FileColumns.MEDIA_TYPE_VIDEO;
    991                     } else if (MediaFile.isImageFileType(fileType)) {
    992                         mediaType = FileColumns.MEDIA_TYPE_IMAGE;
    993                     } else if (MediaFile.isPlayListFileType(fileType)) {
    994                         mediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
    995                     }
    996                     values.put(FileColumns.MEDIA_TYPE, mediaType);
    997                 }
    998                 mMediaProvider.update(mPackageName, result, values, null, null);
    999             }
   1000 
   1001             if(needToSetSettings) {
   1002                 if (notifications) {
   1003                     setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
   1004                     mDefaultNotificationSet = true;
   1005                 } else if (ringtones) {
   1006                     setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
   1007                     mDefaultRingtoneSet = true;
   1008                 } else if (alarms) {
   1009                     setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
   1010                     mDefaultAlarmSet = true;
   1011                 }
   1012             }
   1013 
   1014             return result;
   1015         }
   1016 
   1017         private boolean doesPathHaveFilename(String path, String filename) {
   1018             int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1;
   1019             int filenameLength = filename.length();
   1020             return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) &&
   1021                     pathFilenameStart + filenameLength == path.length();
   1022         }
   1023 
   1024         private void setSettingIfNotSet(String settingName, Uri uri, long rowId) {
   1025 
   1026             String existingSettingValue = Settings.System.getString(mContext.getContentResolver(),
   1027                     settingName);
   1028 
   1029             if (TextUtils.isEmpty(existingSettingValue)) {
   1030                 // Set the setting to the given URI
   1031                 Settings.System.putString(mContext.getContentResolver(), settingName,
   1032                         ContentUris.withAppendedId(uri, rowId).toString());
   1033             }
   1034         }
   1035 
   1036         private int getFileTypeFromDrm(String path) {
   1037             if (!isDrmEnabled()) {
   1038                 return 0;
   1039             }
   1040 
   1041             int resultFileType = 0;
   1042 
   1043             if (mDrmManagerClient == null) {
   1044                 mDrmManagerClient = new DrmManagerClient(mContext);
   1045             }
   1046 
   1047             if (mDrmManagerClient.canHandle(path, null)) {
   1048                 mIsDrm = true;
   1049                 String drmMimetype = mDrmManagerClient.getOriginalMimeType(path);
   1050                 if (drmMimetype != null) {
   1051                     mMimeType = drmMimetype;
   1052                     resultFileType = MediaFile.getFileTypeForMimeType(drmMimetype);
   1053                 }
   1054             }
   1055             return resultFileType;
   1056         }
   1057 
   1058     }; // end of anonymous MediaScannerClient instance
   1059 
   1060     private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
   1061         Cursor c = null;
   1062         String where = null;
   1063         String[] selectionArgs = null;
   1064 
   1065         if (mPlayLists == null) {
   1066             mPlayLists = new ArrayList<FileEntry>();
   1067         } else {
   1068             mPlayLists.clear();
   1069         }
   1070 
   1071         if (filePath != null) {
   1072             // query for only one file
   1073             where = MediaStore.Files.FileColumns._ID + ">?" +
   1074                 " AND " + Files.FileColumns.DATA + "=?";
   1075             selectionArgs = new String[] { "", filePath };
   1076         } else {
   1077             where = MediaStore.Files.FileColumns._ID + ">?";
   1078             selectionArgs = new String[] { "" };
   1079         }
   1080 
   1081         // Tell the provider to not delete the file.
   1082         // If the file is truly gone the delete is unnecessary, and we want to avoid
   1083         // accidentally deleting files that are really there (this may happen if the
   1084         // filesystem is mounted and unmounted while the scanner is running).
   1085         Uri.Builder builder = mFilesUri.buildUpon();
   1086         builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false");
   1087         MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, mPackageName,
   1088                 builder.build());
   1089 
   1090         // Build the list of files from the content provider
   1091         try {
   1092             if (prescanFiles) {
   1093                 // First read existing files from the files table.
   1094                 // Because we'll be deleting entries for missing files as we go,
   1095                 // we need to query the database in small batches, to avoid problems
   1096                 // with CursorWindow positioning.
   1097                 long lastId = Long.MIN_VALUE;
   1098                 Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build();
   1099                 mWasEmptyPriorToScan = true;
   1100 
   1101                 while (true) {
   1102                     selectionArgs[0] = "" + lastId;
   1103                     if (c != null) {
   1104                         c.close();
   1105                         c = null;
   1106                     }
   1107                     c = mMediaProvider.query(mPackageName, limitUri, FILES_PRESCAN_PROJECTION,
   1108                             where, selectionArgs, MediaStore.Files.FileColumns._ID, null);
   1109                     if (c == null) {
   1110                         break;
   1111                     }
   1112 
   1113                     int num = c.getCount();
   1114 
   1115                     if (num == 0) {
   1116                         break;
   1117                     }
   1118                     mWasEmptyPriorToScan = false;
   1119                     while (c.moveToNext()) {
   1120                         long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
   1121                         String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
   1122                         int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
   1123                         long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
   1124                         lastId = rowId;
   1125 
   1126                         // Only consider entries with absolute path names.
   1127                         // This allows storing URIs in the database without the
   1128                         // media scanner removing them.
   1129                         if (path != null && path.startsWith("/")) {
   1130                             boolean exists = false;
   1131                             try {
   1132                                 exists = Libcore.os.access(path, libcore.io.OsConstants.F_OK);
   1133                             } catch (ErrnoException e1) {
   1134                             }
   1135                             if (!exists && !MtpConstants.isAbstractObject(format)) {
   1136                                 // do not delete missing playlists, since they may have been
   1137                                 // modified by the user.
   1138                                 // The user can delete them in the media player instead.
   1139                                 // instead, clear the path and lastModified fields in the row
   1140                                 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
   1141                                 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
   1142 
   1143                                 if (!MediaFile.isPlayListFileType(fileType)) {
   1144                                     deleter.delete(rowId);
   1145                                     if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
   1146                                         deleter.flush();
   1147                                         String parent = new File(path).getParent();
   1148                                         mMediaProvider.call(mPackageName, MediaStore.UNHIDE_CALL,
   1149                                                 parent, null);
   1150                                     }
   1151                                 }
   1152                             }
   1153                         }
   1154                     }
   1155                 }
   1156             }
   1157         }
   1158         finally {
   1159             if (c != null) {
   1160                 c.close();
   1161             }
   1162             deleter.flush();
   1163         }
   1164 
   1165         // compute original size of images
   1166         mOriginalCount = 0;
   1167         c = mMediaProvider.query(mPackageName, mImagesUri, ID_PROJECTION, null, null, null, null);
   1168         if (c != null) {
   1169             mOriginalCount = c.getCount();
   1170             c.close();
   1171         }
   1172     }
   1173 
   1174     private boolean inScanDirectory(String path, String[] directories) {
   1175         for (int i = 0; i < directories.length; i++) {
   1176             String directory = directories[i];
   1177             if (path.startsWith(directory)) {
   1178                 return true;
   1179             }
   1180         }
   1181         return false;
   1182     }
   1183 
   1184     private void pruneDeadThumbnailFiles() {
   1185         HashSet<String> existingFiles = new HashSet<String>();
   1186         String directory = "/sdcard/DCIM/.thumbnails";
   1187         String [] files = (new File(directory)).list();
   1188         if (files == null)
   1189             files = new String[0];
   1190 
   1191         for (int i = 0; i < files.length; i++) {
   1192             String fullPathString = directory + "/" + files[i];
   1193             existingFiles.add(fullPathString);
   1194         }
   1195 
   1196         try {
   1197             Cursor c = mMediaProvider.query(
   1198                     mPackageName,
   1199                     mThumbsUri,
   1200                     new String [] { "_data" },
   1201                     null,
   1202                     null,
   1203                     null, null);
   1204             Log.v(TAG, "pruneDeadThumbnailFiles... " + c);
   1205             if (c != null && c.moveToFirst()) {
   1206                 do {
   1207                     String fullPathString = c.getString(0);
   1208                     existingFiles.remove(fullPathString);
   1209                 } while (c.moveToNext());
   1210             }
   1211 
   1212             for (String fileToDelete : existingFiles) {
   1213                 if (false)
   1214                     Log.v(TAG, "fileToDelete is " + fileToDelete);
   1215                 try {
   1216                     (new File(fileToDelete)).delete();
   1217                 } catch (SecurityException ex) {
   1218                 }
   1219             }
   1220 
   1221             Log.v(TAG, "/pruneDeadThumbnailFiles... " + c);
   1222             if (c != null) {
   1223                 c.close();
   1224             }
   1225         } catch (RemoteException e) {
   1226             // We will soon be killed...
   1227         }
   1228     }
   1229 
   1230     static class MediaBulkDeleter {
   1231         StringBuilder whereClause = new StringBuilder();
   1232         ArrayList<String> whereArgs = new ArrayList<String>(100);
   1233         final IContentProvider mProvider;
   1234         final String mPackageName;
   1235         final Uri mBaseUri;
   1236 
   1237         public MediaBulkDeleter(IContentProvider provider, String packageName, Uri baseUri) {
   1238             mProvider = provider;
   1239             mPackageName = packageName;
   1240             mBaseUri = baseUri;
   1241         }
   1242 
   1243         public void delete(long id) throws RemoteException {
   1244             if (whereClause.length() != 0) {
   1245                 whereClause.append(",");
   1246             }
   1247             whereClause.append("?");
   1248             whereArgs.add("" + id);
   1249             if (whereArgs.size() > 100) {
   1250                 flush();
   1251             }
   1252         }
   1253         public void flush() throws RemoteException {
   1254             int size = whereArgs.size();
   1255             if (size > 0) {
   1256                 String [] foo = new String [size];
   1257                 foo = whereArgs.toArray(foo);
   1258                 int numrows = mProvider.delete(mPackageName, mBaseUri,
   1259                         MediaStore.MediaColumns._ID + " IN (" +
   1260                         whereClause.toString() + ")", foo);
   1261                 //Log.i("@@@@@@@@@", "rows deleted: " + numrows);
   1262                 whereClause.setLength(0);
   1263                 whereArgs.clear();
   1264             }
   1265         }
   1266     }
   1267 
   1268     private void postscan(String[] directories) throws RemoteException {
   1269 
   1270         // handle playlists last, after we know what media files are on the storage.
   1271         if (mProcessPlaylists) {
   1272             processPlayLists();
   1273         }
   1274 
   1275         if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
   1276             pruneDeadThumbnailFiles();
   1277 
   1278         // allow GC to clean up
   1279         mPlayLists = null;
   1280         mMediaProvider = null;
   1281     }
   1282 
   1283     private void initialize(String volumeName) {
   1284         mMediaProvider = mContext.getContentResolver().acquireProvider("media");
   1285 
   1286         mAudioUri = Audio.Media.getContentUri(volumeName);
   1287         mVideoUri = Video.Media.getContentUri(volumeName);
   1288         mImagesUri = Images.Media.getContentUri(volumeName);
   1289         mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
   1290         mFilesUri = Files.getContentUri(volumeName);
   1291         mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build();
   1292 
   1293         if (!volumeName.equals("internal")) {
   1294             // we only support playlists on external media
   1295             mProcessPlaylists = true;
   1296             mProcessGenres = true;
   1297             mPlaylistsUri = Playlists.getContentUri(volumeName);
   1298 
   1299             mCaseInsensitivePaths = true;
   1300         }
   1301     }
   1302 
   1303     public void scanDirectories(String[] directories, String volumeName) {
   1304         try {
   1305             long start = System.currentTimeMillis();
   1306             initialize(volumeName);
   1307             prescan(null, true);
   1308             long prescan = System.currentTimeMillis();
   1309 
   1310             if (ENABLE_BULK_INSERTS) {
   1311                 // create MediaInserter for bulk inserts
   1312                 mMediaInserter = new MediaInserter(mMediaProvider, mPackageName, 500);
   1313             }
   1314 
   1315             for (int i = 0; i < directories.length; i++) {
   1316                 processDirectory(directories[i], mClient);
   1317             }
   1318 
   1319             if (ENABLE_BULK_INSERTS) {
   1320                 // flush remaining inserts
   1321                 mMediaInserter.flushAll();
   1322                 mMediaInserter = null;
   1323             }
   1324 
   1325             long scan = System.currentTimeMillis();
   1326             postscan(directories);
   1327             long end = System.currentTimeMillis();
   1328 
   1329             if (false) {
   1330                 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n");
   1331                 Log.d(TAG, "    scan time: " + (scan - prescan) + "ms\n");
   1332                 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n");
   1333                 Log.d(TAG, "   total time: " + (end - start) + "ms\n");
   1334             }
   1335         } catch (SQLException e) {
   1336             // this might happen if the SD card is removed while the media scanner is running
   1337             Log.e(TAG, "SQLException in MediaScanner.scan()", e);
   1338         } catch (UnsupportedOperationException e) {
   1339             // this might happen if the SD card is removed while the media scanner is running
   1340             Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e);
   1341         } catch (RemoteException e) {
   1342             Log.e(TAG, "RemoteException in MediaScanner.scan()", e);
   1343         }
   1344     }
   1345 
   1346     // this function is used to scan a single file
   1347     public Uri scanSingleFile(String path, String volumeName, String mimeType) {
   1348         try {
   1349             initialize(volumeName);
   1350             prescan(path, true);
   1351 
   1352             File file = new File(path);
   1353             if (!file.exists()) {
   1354                 return null;
   1355             }
   1356 
   1357             // lastModified is in milliseconds on Files.
   1358             long lastModifiedSeconds = file.lastModified() / 1000;
   1359 
   1360             // always scan the file, so we can return the content://media Uri for existing files
   1361             return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(),
   1362                     false, true, MediaScanner.isNoMediaPath(path));
   1363         } catch (RemoteException e) {
   1364             Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
   1365             return null;
   1366         }
   1367     }
   1368 
   1369     private static boolean isNoMediaFile(String path) {
   1370         File file = new File(path);
   1371         if (file.isDirectory()) return false;
   1372 
   1373         // special case certain file names
   1374         // I use regionMatches() instead of substring() below
   1375         // to avoid memory allocation
   1376         int lastSlash = path.lastIndexOf('/');
   1377         if (lastSlash >= 0 && lastSlash + 2 < path.length()) {
   1378             // ignore those ._* files created by MacOS
   1379             if (path.regionMatches(lastSlash + 1, "._", 0, 2)) {
   1380                 return true;
   1381             }
   1382 
   1383             // ignore album art files created by Windows Media Player:
   1384             // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg
   1385             // and AlbumArt_{...}_Small.jpg
   1386             if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) {
   1387                 if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) ||
   1388                         path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) {
   1389                     return true;
   1390                 }
   1391                 int length = path.length() - lastSlash - 1;
   1392                 if ((length == 17 && path.regionMatches(
   1393                         true, lastSlash + 1, "AlbumArtSmall", 0, 13)) ||
   1394                         (length == 10
   1395                          && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) {
   1396                     return true;
   1397                 }
   1398             }
   1399         }
   1400         return false;
   1401     }
   1402 
   1403     public static boolean isNoMediaPath(String path) {
   1404         if (path == null) return false;
   1405 
   1406         // return true if file or any parent directory has name starting with a dot
   1407         if (path.indexOf("/.") >= 0) return true;
   1408 
   1409         // now check to see if any parent directories have a ".nomedia" file
   1410         // start from 1 so we don't bother checking in the root directory
   1411         int offset = 1;
   1412         while (offset >= 0) {
   1413             int slashIndex = path.indexOf('/', offset);
   1414             if (slashIndex > offset) {
   1415                 slashIndex++; // move past slash
   1416                 File file = new File(path.substring(0, slashIndex) + ".nomedia");
   1417                 if (file.exists()) {
   1418                     // we have a .nomedia in one of the parent directories
   1419                     return true;
   1420                 }
   1421             }
   1422             offset = slashIndex;
   1423         }
   1424         return isNoMediaFile(path);
   1425     }
   1426 
   1427     public void scanMtpFile(String path, String volumeName, int objectHandle, int format) {
   1428         initialize(volumeName);
   1429         MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
   1430         int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
   1431         File file = new File(path);
   1432         long lastModifiedSeconds = file.lastModified() / 1000;
   1433 
   1434         if (!MediaFile.isAudioFileType(fileType) && !MediaFile.isVideoFileType(fileType) &&
   1435             !MediaFile.isImageFileType(fileType) && !MediaFile.isPlayListFileType(fileType) &&
   1436             !MediaFile.isDrmFileType(fileType)) {
   1437 
   1438             // no need to use the media scanner, but we need to update last modified and file size
   1439             ContentValues values = new ContentValues();
   1440             values.put(Files.FileColumns.SIZE, file.length());
   1441             values.put(Files.FileColumns.DATE_MODIFIED, lastModifiedSeconds);
   1442             try {
   1443                 String[] whereArgs = new String[] {  Integer.toString(objectHandle) };
   1444                 mMediaProvider.update(mPackageName, Files.getMtpObjectsUri(volumeName), values,
   1445                         "_id=?", whereArgs);
   1446             } catch (RemoteException e) {
   1447                 Log.e(TAG, "RemoteException in scanMtpFile", e);
   1448             }
   1449             return;
   1450         }
   1451 
   1452         mMtpObjectHandle = objectHandle;
   1453         Cursor fileList = null;
   1454         try {
   1455             if (MediaFile.isPlayListFileType(fileType)) {
   1456                 // build file cache so we can look up tracks in the playlist
   1457                 prescan(null, true);
   1458 
   1459                 FileEntry entry = makeEntryFor(path);
   1460                 if (entry != null) {
   1461                     fileList = mMediaProvider.query(mPackageName, mFilesUri,
   1462                             FILES_PRESCAN_PROJECTION, null, null, null, null);
   1463                     processPlayList(entry, fileList);
   1464                 }
   1465             } else {
   1466                 // MTP will create a file entry for us so we don't want to do it in prescan
   1467                 prescan(path, false);
   1468 
   1469                 // always scan the file, so we can return the content://media Uri for existing files
   1470                 mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(),
   1471                     (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path));
   1472             }
   1473         } catch (RemoteException e) {
   1474             Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
   1475         } finally {
   1476             mMtpObjectHandle = 0;
   1477             if (fileList != null) {
   1478                 fileList.close();
   1479             }
   1480         }
   1481     }
   1482 
   1483     FileEntry makeEntryFor(String path) {
   1484         String where;
   1485         String[] selectionArgs;
   1486 
   1487         Cursor c = null;
   1488         try {
   1489             where = Files.FileColumns.DATA + "=?";
   1490             selectionArgs = new String[] { path };
   1491             c = mMediaProvider.query(mPackageName, mFilesUriNoNotify, FILES_PRESCAN_PROJECTION,
   1492                     where, selectionArgs, null, null);
   1493             if (c.moveToFirst()) {
   1494                 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
   1495                 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
   1496                 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
   1497                 return new FileEntry(rowId, path, lastModified, format);
   1498             }
   1499         } catch (RemoteException e) {
   1500         } finally {
   1501             if (c != null) {
   1502                 c.close();
   1503             }
   1504         }
   1505         return null;
   1506     }
   1507 
   1508     // returns the number of matching file/directory names, starting from the right
   1509     private int matchPaths(String path1, String path2) {
   1510         int result = 0;
   1511         int end1 = path1.length();
   1512         int end2 = path2.length();
   1513 
   1514         while (end1 > 0 && end2 > 0) {
   1515             int slash1 = path1.lastIndexOf('/', end1 - 1);
   1516             int slash2 = path2.lastIndexOf('/', end2 - 1);
   1517             int backSlash1 = path1.lastIndexOf('\\', end1 - 1);
   1518             int backSlash2 = path2.lastIndexOf('\\', end2 - 1);
   1519             int start1 = (slash1 > backSlash1 ? slash1 : backSlash1);
   1520             int start2 = (slash2 > backSlash2 ? slash2 : backSlash2);
   1521             if (start1 < 0) start1 = 0; else start1++;
   1522             if (start2 < 0) start2 = 0; else start2++;
   1523             int length = end1 - start1;
   1524             if (end2 - start2 != length) break;
   1525             if (path1.regionMatches(true, start1, path2, start2, length)) {
   1526                 result++;
   1527                 end1 = start1 - 1;
   1528                 end2 = start2 - 1;
   1529             } else break;
   1530         }
   1531 
   1532         return result;
   1533     }
   1534 
   1535     private boolean matchEntries(long rowId, String data) {
   1536 
   1537         int len = mPlaylistEntries.size();
   1538         boolean done = true;
   1539         for (int i = 0; i < len; i++) {
   1540             PlaylistEntry entry = mPlaylistEntries.get(i);
   1541             if (entry.bestmatchlevel == Integer.MAX_VALUE) {
   1542                 continue; // this entry has been matched already
   1543             }
   1544             done = false;
   1545             if (data.equalsIgnoreCase(entry.path)) {
   1546                 entry.bestmatchid = rowId;
   1547                 entry.bestmatchlevel = Integer.MAX_VALUE;
   1548                 continue; // no need for path matching
   1549             }
   1550 
   1551             int matchLength = matchPaths(data, entry.path);
   1552             if (matchLength > entry.bestmatchlevel) {
   1553                 entry.bestmatchid = rowId;
   1554                 entry.bestmatchlevel = matchLength;
   1555             }
   1556         }
   1557         return done;
   1558     }
   1559 
   1560     private void cachePlaylistEntry(String line, String playListDirectory) {
   1561         PlaylistEntry entry = new PlaylistEntry();
   1562         // watch for trailing whitespace
   1563         int entryLength = line.length();
   1564         while (entryLength > 0 && Character.isWhitespace(line.charAt(entryLength - 1))) entryLength--;
   1565         // path should be longer than 3 characters.
   1566         // avoid index out of bounds errors below by returning here.
   1567         if (entryLength < 3) return;
   1568         if (entryLength < line.length()) line = line.substring(0, entryLength);
   1569 
   1570         // does entry appear to be an absolute path?
   1571         // look for Unix or DOS absolute paths
   1572         char ch1 = line.charAt(0);
   1573         boolean fullPath = (ch1 == '/' ||
   1574                 (Character.isLetter(ch1) && line.charAt(1) == ':' && line.charAt(2) == '\\'));
   1575         // if we have a relative path, combine entry with playListDirectory
   1576         if (!fullPath)
   1577             line = playListDirectory + line;
   1578         entry.path = line;
   1579         //FIXME - should we look for "../" within the path?
   1580 
   1581         mPlaylistEntries.add(entry);
   1582     }
   1583 
   1584     private void processCachedPlaylist(Cursor fileList, ContentValues values, Uri playlistUri) {
   1585         fileList.moveToPosition(-1);
   1586         while (fileList.moveToNext()) {
   1587             long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
   1588             String data = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
   1589             if (matchEntries(rowId, data)) {
   1590                 break;
   1591             }
   1592         }
   1593 
   1594         int len = mPlaylistEntries.size();
   1595         int index = 0;
   1596         for (int i = 0; i < len; i++) {
   1597             PlaylistEntry entry = mPlaylistEntries.get(i);
   1598             if (entry.bestmatchlevel > 0) {
   1599                 try {
   1600                     values.clear();
   1601                     values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index));
   1602                     values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(entry.bestmatchid));
   1603                     mMediaProvider.insert(mPackageName, playlistUri, values);
   1604                     index++;
   1605                 } catch (RemoteException e) {
   1606                     Log.e(TAG, "RemoteException in MediaScanner.processCachedPlaylist()", e);
   1607                     return;
   1608                 }
   1609             }
   1610         }
   1611         mPlaylistEntries.clear();
   1612     }
   1613 
   1614     private void processM3uPlayList(String path, String playListDirectory, Uri uri,
   1615             ContentValues values, Cursor fileList) {
   1616         BufferedReader reader = null;
   1617         try {
   1618             File f = new File(path);
   1619             if (f.exists()) {
   1620                 reader = new BufferedReader(
   1621                         new InputStreamReader(new FileInputStream(f)), 8192);
   1622                 String line = reader.readLine();
   1623                 mPlaylistEntries.clear();
   1624                 while (line != null) {
   1625                     // ignore comment lines, which begin with '#'
   1626                     if (line.length() > 0 && line.charAt(0) != '#') {
   1627                         cachePlaylistEntry(line, playListDirectory);
   1628                     }
   1629                     line = reader.readLine();
   1630                 }
   1631 
   1632                 processCachedPlaylist(fileList, values, uri);
   1633             }
   1634         } catch (IOException e) {
   1635             Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
   1636         } finally {
   1637             try {
   1638                 if (reader != null)
   1639                     reader.close();
   1640             } catch (IOException e) {
   1641                 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
   1642             }
   1643         }
   1644     }
   1645 
   1646     private void processPlsPlayList(String path, String playListDirectory, Uri uri,
   1647             ContentValues values, Cursor fileList) {
   1648         BufferedReader reader = null;
   1649         try {
   1650             File f = new File(path);
   1651             if (f.exists()) {
   1652                 reader = new BufferedReader(
   1653                         new InputStreamReader(new FileInputStream(f)), 8192);
   1654                 String line = reader.readLine();
   1655                 mPlaylistEntries.clear();
   1656                 while (line != null) {
   1657                     // ignore comment lines, which begin with '#'
   1658                     if (line.startsWith("File")) {
   1659                         int equals = line.indexOf('=');
   1660                         if (equals > 0) {
   1661                             cachePlaylistEntry(line.substring(equals + 1), playListDirectory);
   1662                         }
   1663                     }
   1664                     line = reader.readLine();
   1665                 }
   1666 
   1667                 processCachedPlaylist(fileList, values, uri);
   1668             }
   1669         } catch (IOException e) {
   1670             Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
   1671         } finally {
   1672             try {
   1673                 if (reader != null)
   1674                     reader.close();
   1675             } catch (IOException e) {
   1676                 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
   1677             }
   1678         }
   1679     }
   1680 
   1681     class WplHandler implements ElementListener {
   1682 
   1683         final ContentHandler handler;
   1684         String playListDirectory;
   1685 
   1686         public WplHandler(String playListDirectory, Uri uri, Cursor fileList) {
   1687             this.playListDirectory = playListDirectory;
   1688 
   1689             RootElement root = new RootElement("smil");
   1690             Element body = root.getChild("body");
   1691             Element seq = body.getChild("seq");
   1692             Element media = seq.getChild("media");
   1693             media.setElementListener(this);
   1694 
   1695             this.handler = root.getContentHandler();
   1696         }
   1697 
   1698         @Override
   1699         public void start(Attributes attributes) {
   1700             String path = attributes.getValue("", "src");
   1701             if (path != null) {
   1702                 cachePlaylistEntry(path, playListDirectory);
   1703             }
   1704         }
   1705 
   1706        @Override
   1707        public void end() {
   1708        }
   1709 
   1710         ContentHandler getContentHandler() {
   1711             return handler;
   1712         }
   1713     }
   1714 
   1715     private void processWplPlayList(String path, String playListDirectory, Uri uri,
   1716             ContentValues values, Cursor fileList) {
   1717         FileInputStream fis = null;
   1718         try {
   1719             File f = new File(path);
   1720             if (f.exists()) {
   1721                 fis = new FileInputStream(f);
   1722 
   1723                 mPlaylistEntries.clear();
   1724                 Xml.parse(fis, Xml.findEncodingByName("UTF-8"),
   1725                         new WplHandler(playListDirectory, uri, fileList).getContentHandler());
   1726 
   1727                 processCachedPlaylist(fileList, values, uri);
   1728             }
   1729         } catch (SAXException e) {
   1730             e.printStackTrace();
   1731         } catch (IOException e) {
   1732             e.printStackTrace();
   1733         } finally {
   1734             try {
   1735                 if (fis != null)
   1736                     fis.close();
   1737             } catch (IOException e) {
   1738                 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e);
   1739             }
   1740         }
   1741     }
   1742 
   1743     private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException {
   1744         String path = entry.mPath;
   1745         ContentValues values = new ContentValues();
   1746         int lastSlash = path.lastIndexOf('/');
   1747         if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path);
   1748         Uri uri, membersUri;
   1749         long rowId = entry.mRowId;
   1750 
   1751         // make sure we have a name
   1752         String name = values.getAsString(MediaStore.Audio.Playlists.NAME);
   1753         if (name == null) {
   1754             name = values.getAsString(MediaStore.MediaColumns.TITLE);
   1755             if (name == null) {
   1756                 // extract name from file name
   1757                 int lastDot = path.lastIndexOf('.');
   1758                 name = (lastDot < 0 ? path.substring(lastSlash + 1)
   1759                         : path.substring(lastSlash + 1, lastDot));
   1760             }
   1761         }
   1762 
   1763         values.put(MediaStore.Audio.Playlists.NAME, name);
   1764         values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
   1765 
   1766         if (rowId == 0) {
   1767             values.put(MediaStore.Audio.Playlists.DATA, path);
   1768             uri = mMediaProvider.insert(mPackageName, mPlaylistsUri, values);
   1769             rowId = ContentUris.parseId(uri);
   1770             membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
   1771         } else {
   1772             uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
   1773             mMediaProvider.update(mPackageName, uri, values, null, null);
   1774 
   1775             // delete members of existing playlist
   1776             membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
   1777             mMediaProvider.delete(mPackageName, membersUri, null, null);
   1778         }
   1779 
   1780         String playListDirectory = path.substring(0, lastSlash + 1);
   1781         MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
   1782         int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
   1783 
   1784         if (fileType == MediaFile.FILE_TYPE_M3U) {
   1785             processM3uPlayList(path, playListDirectory, membersUri, values, fileList);
   1786         } else if (fileType == MediaFile.FILE_TYPE_PLS) {
   1787             processPlsPlayList(path, playListDirectory, membersUri, values, fileList);
   1788         } else if (fileType == MediaFile.FILE_TYPE_WPL) {
   1789             processWplPlayList(path, playListDirectory, membersUri, values, fileList);
   1790         }
   1791     }
   1792 
   1793     private void processPlayLists() throws RemoteException {
   1794         Iterator<FileEntry> iterator = mPlayLists.iterator();
   1795         Cursor fileList = null;
   1796         try {
   1797             // use the files uri and projection because we need the format column,
   1798             // but restrict the query to just audio files
   1799             fileList = mMediaProvider.query(mPackageName, mFilesUri, FILES_PRESCAN_PROJECTION,
   1800                     "media_type=2", null, null, null);
   1801             while (iterator.hasNext()) {
   1802                 FileEntry entry = iterator.next();
   1803                 // only process playlist files if they are new or have been modified since the last scan
   1804                 if (entry.mLastModifiedChanged) {
   1805                     processPlayList(entry, fileList);
   1806                 }
   1807             }
   1808         } catch (RemoteException e1) {
   1809         } finally {
   1810             if (fileList != null) {
   1811                 fileList.close();
   1812             }
   1813         }
   1814     }
   1815 
   1816     private native void processDirectory(String path, MediaScannerClient client);
   1817     private native void processFile(String path, String mimeType, MediaScannerClient client);
   1818     public native void setLocale(String locale);
   1819 
   1820     public native byte[] extractAlbumArt(FileDescriptor fd);
   1821 
   1822     private static native final void native_init();
   1823     private native final void native_setup();
   1824     private native final void native_finalize();
   1825 
   1826     /**
   1827      * Releases resources associated with this MediaScanner object.
   1828      * It is considered good practice to call this method when
   1829      * one is done using the MediaScanner object. After this method
   1830      * is called, the MediaScanner object can no longer be used.
   1831      */
   1832     public void release() {
   1833         native_finalize();
   1834     }
   1835 
   1836     @Override
   1837     protected void finalize() {
   1838         mContext.getContentResolver().releaseProvider(mMediaProvider);
   1839         native_finalize();
   1840     }
   1841 }
   1842