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