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