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