MediaScanner.java revision 6231e3a778a4ffe61683abefa2479127f4566d4c
1/* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.media; 18 19import android.content.ContentValues; 20import android.content.Context; 21import android.content.IContentProvider; 22import android.content.ContentUris; 23import android.database.Cursor; 24import android.database.SQLException; 25import android.graphics.BitmapFactory; 26import android.net.Uri; 27import android.os.Process; 28import android.os.RemoteException; 29import android.os.SystemProperties; 30import android.provider.MediaStore; 31import android.provider.Settings; 32import android.provider.MediaStore.Audio; 33import android.provider.MediaStore.Images; 34import android.provider.MediaStore.Video; 35import android.provider.MediaStore.Audio.Genres; 36import android.provider.MediaStore.Audio.Playlists; 37import android.sax.Element; 38import android.sax.ElementListener; 39import android.sax.RootElement; 40import android.text.TextUtils; 41import android.util.Config; 42import android.util.Log; 43import android.util.Xml; 44 45import org.xml.sax.Attributes; 46import org.xml.sax.ContentHandler; 47import org.xml.sax.SAXException; 48 49import java.io.*; 50import java.util.ArrayList; 51import java.util.HashMap; 52import java.util.HashSet; 53import java.util.Iterator; 54 55/** 56 * Internal service helper that no-one should use directly. 57 * 58 * The way the scan currently works is: 59 * - The Java MediaScannerService creates a MediaScanner (this class), and calls 60 * MediaScanner.scanDirectories on it. 61 * - scanDirectories() calls the native processDirectory() for each of the specified directories. 62 * - the processDirectory() JNI method wraps the provided mediascanner client in a native 63 * 'MyMediaScannerClient' class, then calls processDirectory() on the native MediaScanner 64 * object (which got created when the Java MediaScanner was created). 65 * - native MediaScanner.processDirectory() (currently part of opencore) calls 66 * doProcessDirectory(), which recurses over the folder, and calls 67 * native MyMediaScannerClient.scanFile() for every file whose extension matches. 68 * - native MyMediaScannerClient.scanFile() calls back on Java MediaScannerClient.scanFile, 69 * which calls doScanFile, which after some setup calls back down to native code, calling 70 * MediaScanner.processFile(). 71 * - MediaScanner.processFile() calls one of several methods, depending on the type of the 72 * file: parseMP3, parseMP4, parseMidi, parseOgg or parseWMA. 73 * - each of these methods gets metadata key/value pairs from the file, and repeatedly 74 * calls native MyMediaScannerClient.handleStringTag, which calls back up to its Java 75 * counterparts in this file. 76 * - Java handleStringTag() gathers the key/value pairs that it's interested in. 77 * - once processFile returns and we're back in Java code in doScanFile(), it calls 78 * Java MyMediaScannerClient.endFile(), which takes all the data that's been 79 * gathered and inserts an entry in to the database. 80 * 81 * In summary: 82 * Java MediaScannerService calls 83 * Java MediaScanner scanDirectories, which calls 84 * Java MediaScanner processDirectory (native method), which calls 85 * native MediaScanner processDirectory, which calls 86 * native MyMediaScannerClient scanFile, which calls 87 * Java MyMediaScannerClient scanFile, which calls 88 * Java MediaScannerClient doScanFile, which calls 89 * Java MediaScanner processFile (native method), which calls 90 * native MediaScanner processFile, which calls 91 * native parseMP3, parseMP4, parseMidi, parseOgg or parseWMA, which calls 92 * native MyMediaScanner handleStringTag, which calls 93 * Java MyMediaScanner handleStringTag. 94 * Once MediaScanner processFile returns, an entry is inserted in to the database. 95 * 96 * {@hide} 97 */ 98public class MediaScanner 99{ 100 static { 101 System.loadLibrary("media_jni"); 102 } 103 104 private final static String TAG = "MediaScanner"; 105 106 private static final String[] AUDIO_PROJECTION = new String[] { 107 Audio.Media._ID, // 0 108 Audio.Media.DATA, // 1 109 Audio.Media.DATE_MODIFIED, // 2 110 }; 111 112 private static final int ID_AUDIO_COLUMN_INDEX = 0; 113 private static final int PATH_AUDIO_COLUMN_INDEX = 1; 114 private static final int DATE_MODIFIED_AUDIO_COLUMN_INDEX = 2; 115 116 private static final String[] VIDEO_PROJECTION = new String[] { 117 Video.Media._ID, // 0 118 Video.Media.DATA, // 1 119 Video.Media.DATE_MODIFIED, // 2 120 }; 121 122 private static final int ID_VIDEO_COLUMN_INDEX = 0; 123 private static final int PATH_VIDEO_COLUMN_INDEX = 1; 124 private static final int DATE_MODIFIED_VIDEO_COLUMN_INDEX = 2; 125 126 private static final String[] IMAGES_PROJECTION = new String[] { 127 Images.Media._ID, // 0 128 Images.Media.DATA, // 1 129 Images.Media.DATE_MODIFIED, // 2 130 }; 131 132 private static final int ID_IMAGES_COLUMN_INDEX = 0; 133 private static final int PATH_IMAGES_COLUMN_INDEX = 1; 134 private static final int DATE_MODIFIED_IMAGES_COLUMN_INDEX = 2; 135 136 private static final String[] PLAYLISTS_PROJECTION = new String[] { 137 Audio.Playlists._ID, // 0 138 Audio.Playlists.DATA, // 1 139 Audio.Playlists.DATE_MODIFIED, // 2 140 }; 141 142 private static final String[] PLAYLIST_MEMBERS_PROJECTION = new String[] { 143 Audio.Playlists.Members.PLAYLIST_ID, // 0 144 }; 145 146 private static final int ID_PLAYLISTS_COLUMN_INDEX = 0; 147 private static final int PATH_PLAYLISTS_COLUMN_INDEX = 1; 148 private static final int DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX = 2; 149 150 private static final String[] GENRE_LOOKUP_PROJECTION = new String[] { 151 Audio.Genres._ID, // 0 152 Audio.Genres.NAME, // 1 153 }; 154 155 private static final String RINGTONES_DIR = "/ringtones/"; 156 private static final String NOTIFICATIONS_DIR = "/notifications/"; 157 private static final String ALARMS_DIR = "/alarms/"; 158 private static final String MUSIC_DIR = "/music/"; 159 private static final String PODCAST_DIR = "/podcasts/"; 160 161 private static final String[] ID3_GENRES = { 162 // ID3v1 Genres 163 "Blues", 164 "Classic Rock", 165 "Country", 166 "Dance", 167 "Disco", 168 "Funk", 169 "Grunge", 170 "Hip-Hop", 171 "Jazz", 172 "Metal", 173 "New Age", 174 "Oldies", 175 "Other", 176 "Pop", 177 "R&B", 178 "Rap", 179 "Reggae", 180 "Rock", 181 "Techno", 182 "Industrial", 183 "Alternative", 184 "Ska", 185 "Death Metal", 186 "Pranks", 187 "Soundtrack", 188 "Euro-Techno", 189 "Ambient", 190 "Trip-Hop", 191 "Vocal", 192 "Jazz+Funk", 193 "Fusion", 194 "Trance", 195 "Classical", 196 "Instrumental", 197 "Acid", 198 "House", 199 "Game", 200 "Sound Clip", 201 "Gospel", 202 "Noise", 203 "AlternRock", 204 "Bass", 205 "Soul", 206 "Punk", 207 "Space", 208 "Meditative", 209 "Instrumental Pop", 210 "Instrumental Rock", 211 "Ethnic", 212 "Gothic", 213 "Darkwave", 214 "Techno-Industrial", 215 "Electronic", 216 "Pop-Folk", 217 "Eurodance", 218 "Dream", 219 "Southern Rock", 220 "Comedy", 221 "Cult", 222 "Gangsta", 223 "Top 40", 224 "Christian Rap", 225 "Pop/Funk", 226 "Jungle", 227 "Native American", 228 "Cabaret", 229 "New Wave", 230 "Psychadelic", 231 "Rave", 232 "Showtunes", 233 "Trailer", 234 "Lo-Fi", 235 "Tribal", 236 "Acid Punk", 237 "Acid Jazz", 238 "Polka", 239 "Retro", 240 "Musical", 241 "Rock & Roll", 242 "Hard Rock", 243 // The following genres are Winamp extensions 244 "Folk", 245 "Folk-Rock", 246 "National Folk", 247 "Swing", 248 "Fast Fusion", 249 "Bebob", 250 "Latin", 251 "Revival", 252 "Celtic", 253 "Bluegrass", 254 "Avantgarde", 255 "Gothic Rock", 256 "Progressive Rock", 257 "Psychedelic Rock", 258 "Symphonic Rock", 259 "Slow Rock", 260 "Big Band", 261 "Chorus", 262 "Easy Listening", 263 "Acoustic", 264 "Humour", 265 "Speech", 266 "Chanson", 267 "Opera", 268 "Chamber Music", 269 "Sonata", 270 "Symphony", 271 "Booty Bass", 272 "Primus", 273 "Porn Groove", 274 "Satire", 275 "Slow Jam", 276 "Club", 277 "Tango", 278 "Samba", 279 "Folklore", 280 "Ballad", 281 "Power Ballad", 282 "Rhythmic Soul", 283 "Freestyle", 284 "Duet", 285 "Punk Rock", 286 "Drum Solo", 287 "A capella", 288 "Euro-House", 289 "Dance Hall" 290 }; 291 292 private int mNativeContext; 293 private Context mContext; 294 private IContentProvider mMediaProvider; 295 private Uri mAudioUri; 296 private Uri mVideoUri; 297 private Uri mImagesUri; 298 private Uri mThumbsUri; 299 private Uri mGenresUri; 300 private Uri mPlaylistsUri; 301 private boolean mProcessPlaylists, mProcessGenres; 302 303 // used when scanning the image database so we know whether we have to prune 304 // old thumbnail files 305 private int mOriginalCount; 306 /** Whether the scanner has set a default sound for the ringer ringtone. */ 307 private boolean mDefaultRingtoneSet; 308 /** Whether the scanner has set a default sound for the notification ringtone. */ 309 private boolean mDefaultNotificationSet; 310 /** Whether the scanner has set a default sound for the alarm ringtone. */ 311 private boolean mDefaultAlarmSet; 312 /** The filename for the default sound for the ringer ringtone. */ 313 private String mDefaultRingtoneFilename; 314 /** The filename for the default sound for the notification ringtone. */ 315 private String mDefaultNotificationFilename; 316 /** The filename for the default sound for the alarm ringtone. */ 317 private String mDefaultAlarmAlertFilename; 318 /** 319 * The prefix for system properties that define the default sound for 320 * ringtones. Concatenate the name of the setting from Settings 321 * to get the full system property. 322 */ 323 private static final String DEFAULT_RINGTONE_PROPERTY_PREFIX = "ro.config."; 324 325 // set to true if file path comparisons should be case insensitive. 326 // this should be set when scanning files on a case insensitive file system. 327 private boolean mCaseInsensitivePaths; 328 329 private BitmapFactory.Options mBitmapOptions = new BitmapFactory.Options(); 330 331 private static class FileCacheEntry { 332 Uri mTableUri; 333 long mRowId; 334 String mPath; 335 long mLastModified; 336 boolean mSeenInFileSystem; 337 boolean mLastModifiedChanged; 338 339 FileCacheEntry(Uri tableUri, long rowId, String path, long lastModified) { 340 mTableUri = tableUri; 341 mRowId = rowId; 342 mPath = path; 343 mLastModified = lastModified; 344 mSeenInFileSystem = false; 345 mLastModifiedChanged = false; 346 } 347 348 @Override 349 public String toString() { 350 return mPath; 351 } 352 } 353 354 // hashes file path to FileCacheEntry. 355 // path should be lower case if mCaseInsensitivePaths is true 356 private HashMap<String, FileCacheEntry> mFileCache; 357 358 private ArrayList<FileCacheEntry> mPlayLists; 359 private HashMap<String, Uri> mGenreCache; 360 361 362 public MediaScanner(Context c) { 363 native_setup(); 364 mContext = c; 365 mBitmapOptions.inSampleSize = 1; 366 mBitmapOptions.inJustDecodeBounds = true; 367 368 setDefaultRingtoneFileNames(); 369 } 370 371 private void setDefaultRingtoneFileNames() { 372 mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 373 + Settings.System.RINGTONE); 374 mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 375 + Settings.System.NOTIFICATION_SOUND); 376 mDefaultAlarmAlertFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 377 + Settings.System.ALARM_ALERT); 378 } 379 380 private MyMediaScannerClient mClient = new MyMediaScannerClient(); 381 382 private class MyMediaScannerClient implements MediaScannerClient { 383 384 private String mArtist; 385 private String mAlbumArtist; // use this if mArtist is missing 386 private String mAlbum; 387 private String mTitle; 388 private String mComposer; 389 private String mGenre; 390 private String mMimeType; 391 private int mFileType; 392 private int mTrack; 393 private int mYear; 394 private int mDuration; 395 private String mPath; 396 private long mLastModified; 397 private long mFileSize; 398 399 public FileCacheEntry beginFile(String path, String mimeType, long lastModified, long fileSize) { 400 401 // special case certain file names 402 // I use regionMatches() instead of substring() below 403 // to avoid memory allocation 404 int lastSlash = path.lastIndexOf('/'); 405 if (lastSlash >= 0 && lastSlash + 2 < path.length()) { 406 // ignore those ._* files created by MacOS 407 if (path.regionMatches(lastSlash + 1, "._", 0, 2)) { 408 return null; 409 } 410 411 // ignore album art files created by Windows Media Player: 412 // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg and AlbumArt_{...}_Small.jpg 413 if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) { 414 if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) || 415 path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) { 416 return null; 417 } 418 int length = path.length() - lastSlash - 1; 419 if ((length == 17 && path.regionMatches(true, lastSlash + 1, "AlbumArtSmall", 0, 13)) || 420 (length == 10 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) { 421 return null; 422 } 423 } 424 } 425 426 mMimeType = null; 427 // try mimeType first, if it is specified 428 if (mimeType != null) { 429 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 430 if (mFileType != 0) { 431 mMimeType = mimeType; 432 } 433 } 434 mFileSize = fileSize; 435 436 // if mimeType was not specified, compute file type based on file extension. 437 if (mMimeType == null) { 438 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 439 if (mediaFileType != null) { 440 mFileType = mediaFileType.fileType; 441 mMimeType = mediaFileType.mimeType; 442 } 443 } 444 445 String key = path; 446 if (mCaseInsensitivePaths) { 447 key = path.toLowerCase(); 448 } 449 FileCacheEntry entry = mFileCache.get(key); 450 if (entry == null) { 451 entry = new FileCacheEntry(null, 0, path, 0); 452 mFileCache.put(key, entry); 453 } 454 entry.mSeenInFileSystem = true; 455 456 // add some slack to avoid a rounding error 457 long delta = lastModified - entry.mLastModified; 458 if (delta > 1 || delta < -1) { 459 entry.mLastModified = lastModified; 460 entry.mLastModifiedChanged = true; 461 } 462 463 if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) { 464 mPlayLists.add(entry); 465 // we don't process playlists in the main scan, so return null 466 return null; 467 } 468 469 // clear all the metadata 470 mArtist = null; 471 mAlbumArtist = null; 472 mAlbum = null; 473 mTitle = null; 474 mComposer = null; 475 mGenre = null; 476 mTrack = 0; 477 mYear = 0; 478 mDuration = 0; 479 mPath = path; 480 mLastModified = lastModified; 481 482 return entry; 483 } 484 485 public void scanFile(String path, long lastModified, long fileSize) { 486 doScanFile(path, null, lastModified, fileSize, false); 487 } 488 489 public void scanFile(String path, String mimeType, long lastModified, long fileSize) { 490 doScanFile(path, mimeType, lastModified, fileSize, false); 491 } 492 493 public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean scanAlways) { 494 Uri result = null; 495// long t1 = System.currentTimeMillis(); 496 try { 497 FileCacheEntry entry = beginFile(path, mimeType, lastModified, fileSize); 498 // rescan for metadata if file was modified since last scan 499 if (entry != null && (entry.mLastModifiedChanged || scanAlways)) { 500 String lowpath = path.toLowerCase(); 501 boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0); 502 boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0); 503 boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0); 504 boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0); 505 boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) || 506 (!ringtones && !notifications && !alarms && !podcasts); 507 508 if (mFileType == MediaFile.FILE_TYPE_MP3 || 509 mFileType == MediaFile.FILE_TYPE_MP4 || 510 mFileType == MediaFile.FILE_TYPE_M4A || 511 mFileType == MediaFile.FILE_TYPE_3GPP || 512 mFileType == MediaFile.FILE_TYPE_3GPP2 || 513 mFileType == MediaFile.FILE_TYPE_OGG || 514 mFileType == MediaFile.FILE_TYPE_MID || 515 mFileType == MediaFile.FILE_TYPE_WMA) { 516 // we only extract metadata from MP3, M4A, OGG, MID and WMA files. 517 // check MP4 files, to determine if they contain only audio. 518 processFile(path, mimeType, this); 519 } else if (MediaFile.isImageFileType(mFileType)) { 520 // we used to compute the width and height but it's not worth it 521 } 522 523 result = endFile(entry, ringtones, notifications, alarms, music, podcasts); 524 } 525 } catch (RemoteException e) { 526 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 527 } 528// long t2 = System.currentTimeMillis(); 529// Log.v(TAG, "scanFile: " + path + " took " + (t2-t1)); 530 return result; 531 } 532 533 private int parseSubstring(String s, int start, int defaultValue) { 534 int length = s.length(); 535 if (start == length) return defaultValue; 536 537 char ch = s.charAt(start++); 538 // return defaultValue if we have no integer at all 539 if (ch < '0' || ch > '9') return defaultValue; 540 541 int result = ch - '0'; 542 while (start < length) { 543 ch = s.charAt(start++); 544 if (ch < '0' || ch > '9') return result; 545 result = result * 10 + (ch - '0'); 546 } 547 548 return result; 549 } 550 551 public void handleStringTag(String name, String value) { 552 if (name.equalsIgnoreCase("title") || name.startsWith("title;")) { 553 // Don't trim() here, to preserve the special \001 character 554 // used to force sorting. The media provider will trim() before 555 // inserting the title in to the database. 556 mTitle = value; 557 } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) { 558 mArtist = value.trim(); 559 } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")) { 560 mAlbumArtist = value.trim(); 561 } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) { 562 mAlbum = value.trim(); 563 } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) { 564 mComposer = value.trim(); 565 } else if (name.equalsIgnoreCase("genre") || name.startsWith("genre;")) { 566 // handle numeric genres, which PV sometimes encodes like "(20)" 567 if (value.length() > 0) { 568 int genreCode = -1; 569 char ch = value.charAt(0); 570 if (ch == '(') { 571 genreCode = parseSubstring(value, 1, -1); 572 } else if (ch >= '0' && ch <= '9') { 573 genreCode = parseSubstring(value, 0, -1); 574 } 575 if (genreCode >= 0 && genreCode < ID3_GENRES.length) { 576 value = ID3_GENRES[genreCode]; 577 } 578 } 579 mGenre = value; 580 } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) { 581 mYear = parseSubstring(value, 0, 0); 582 } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) { 583 // track number might be of the form "2/12" 584 // we just read the number before the slash 585 int num = parseSubstring(value, 0, 0); 586 mTrack = (mTrack / 1000) * 1000 + num; 587 } else if (name.equalsIgnoreCase("discnumber") || 588 name.equals("set") || name.startsWith("set;")) { 589 // set number might be of the form "1/3" 590 // we just read the number before the slash 591 int num = parseSubstring(value, 0, 0); 592 mTrack = (num * 1000) + (mTrack % 1000); 593 } else if (name.equalsIgnoreCase("duration")) { 594 mDuration = parseSubstring(value, 0, 0); 595 } 596 } 597 598 public void setMimeType(String mimeType) { 599 mMimeType = mimeType; 600 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 601 } 602 603 /** 604 * Formats the data into a values array suitable for use with the Media 605 * Content Provider. 606 * 607 * @return a map of values 608 */ 609 private ContentValues toValues() { 610 ContentValues map = new ContentValues(); 611 612 map.put(MediaStore.MediaColumns.DATA, mPath); 613 map.put(MediaStore.MediaColumns.TITLE, mTitle); 614 map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified); 615 map.put(MediaStore.MediaColumns.SIZE, mFileSize); 616 map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType); 617 618 if (MediaFile.isVideoFileType(mFileType)) { 619 map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaFile.UNKNOWN_STRING)); 620 map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaFile.UNKNOWN_STRING)); 621 map.put(Video.Media.DURATION, mDuration); 622 // FIXME - add RESOLUTION 623 } else if (MediaFile.isImageFileType(mFileType)) { 624 // FIXME - add DESCRIPTION 625 // map.put(field, value); 626 } else if (MediaFile.isAudioFileType(mFileType)) { 627 map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaFile.UNKNOWN_STRING)); 628 map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaFile.UNKNOWN_STRING)); 629 map.put(Audio.Media.COMPOSER, mComposer); 630 if (mYear != 0) { 631 map.put(Audio.Media.YEAR, mYear); 632 } 633 map.put(Audio.Media.TRACK, mTrack); 634 map.put(Audio.Media.DURATION, mDuration); 635 } 636 return map; 637 } 638 639 private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications, 640 boolean alarms, boolean music, boolean podcasts) 641 throws RemoteException { 642 // update database 643 Uri tableUri; 644 boolean isAudio = MediaFile.isAudioFileType(mFileType); 645 boolean isVideo = MediaFile.isVideoFileType(mFileType); 646 boolean isImage = MediaFile.isImageFileType(mFileType); 647 if (isVideo) { 648 tableUri = mVideoUri; 649 } else if (isImage) { 650 tableUri = mImagesUri; 651 } else if (isAudio) { 652 tableUri = mAudioUri; 653 } else { 654 // don't add file to database if not audio, video or image 655 return null; 656 } 657 entry.mTableUri = tableUri; 658 659 // use album artist if artist is missing 660 if (mArtist == null || mArtist.length() == 0) { 661 mArtist = mAlbumArtist; 662 } 663 664 ContentValues values = toValues(); 665 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 666 if (TextUtils.isEmpty(title)) { 667 title = values.getAsString(MediaStore.MediaColumns.DATA); 668 // extract file name after last slash 669 int lastSlash = title.lastIndexOf('/'); 670 if (lastSlash >= 0) { 671 lastSlash++; 672 if (lastSlash < title.length()) { 673 title = title.substring(lastSlash); 674 } 675 } 676 // truncate the file extension (if any) 677 int lastDot = title.lastIndexOf('.'); 678 if (lastDot > 0) { 679 title = title.substring(0, lastDot); 680 } 681 values.put(MediaStore.MediaColumns.TITLE, title); 682 } 683 if (isAudio) { 684 values.put(Audio.Media.IS_RINGTONE, ringtones); 685 values.put(Audio.Media.IS_NOTIFICATION, notifications); 686 values.put(Audio.Media.IS_ALARM, alarms); 687 values.put(Audio.Media.IS_MUSIC, music); 688 values.put(Audio.Media.IS_PODCAST, podcasts); 689 } else if (isImage) { 690 // nothing right now 691 } 692 693 Uri result = null; 694 long rowId = entry.mRowId; 695 if (rowId == 0) { 696 // new file, insert it 697 result = mMediaProvider.insert(tableUri, values); 698 if (result != null) { 699 rowId = ContentUris.parseId(result); 700 entry.mRowId = rowId; 701 } 702 } else { 703 // updated file 704 result = ContentUris.withAppendedId(tableUri, rowId); 705 mMediaProvider.update(result, values, null, null); 706 } 707 if (mProcessGenres && mGenre != null) { 708 String genre = mGenre; 709 Uri uri = mGenreCache.get(genre); 710 if (uri == null) { 711 Cursor cursor = null; 712 try { 713 // see if the genre already exists 714 cursor = mMediaProvider.query( 715 mGenresUri, 716 GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?", 717 new String[] { genre }, null); 718 if (cursor == null || cursor.getCount() == 0) { 719 // genre does not exist, so create the genre in the genre table 720 values.clear(); 721 values.put(MediaStore.Audio.Genres.NAME, genre); 722 uri = mMediaProvider.insert(mGenresUri, values); 723 } else { 724 // genre already exists, so compute its Uri 725 cursor.moveToNext(); 726 uri = ContentUris.withAppendedId(mGenresUri, cursor.getLong(0)); 727 } 728 if (uri != null) { 729 uri = Uri.withAppendedPath(uri, Genres.Members.CONTENT_DIRECTORY); 730 mGenreCache.put(genre, uri); 731 } 732 } finally { 733 // release the cursor if it exists 734 if (cursor != null) { 735 cursor.close(); 736 } 737 } 738 } 739 740 if (uri != null) { 741 // add entry to audio_genre_map 742 values.clear(); 743 values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId)); 744 mMediaProvider.insert(uri, values); 745 } 746 } 747 748 if (notifications && !mDefaultNotificationSet) { 749 if (TextUtils.isEmpty(mDefaultNotificationFilename) || 750 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) { 751 setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId); 752 mDefaultNotificationSet = true; 753 } 754 } else if (ringtones && !mDefaultRingtoneSet) { 755 if (TextUtils.isEmpty(mDefaultRingtoneFilename) || 756 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) { 757 setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId); 758 mDefaultRingtoneSet = true; 759 } 760 } else if (alarms && !mDefaultAlarmSet) { 761 if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) || 762 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) { 763 setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId); 764 mDefaultAlarmSet = true; 765 } 766 } 767 768 return result; 769 } 770 771 private boolean doesPathHaveFilename(String path, String filename) { 772 int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1; 773 int filenameLength = filename.length(); 774 return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) && 775 pathFilenameStart + filenameLength == path.length(); 776 } 777 778 private void setSettingIfNotSet(String settingName, Uri uri, long rowId) { 779 780 String existingSettingValue = Settings.System.getString(mContext.getContentResolver(), 781 settingName); 782 783 if (TextUtils.isEmpty(existingSettingValue)) { 784 // Set the setting to the given URI 785 Settings.System.putString(mContext.getContentResolver(), settingName, 786 ContentUris.withAppendedId(uri, rowId).toString()); 787 } 788 } 789 790 }; // end of anonymous MediaScannerClient instance 791 792 private void prescan(String filePath) throws RemoteException { 793 Cursor c = null; 794 String where = null; 795 String[] selectionArgs = null; 796 797 if (mFileCache == null) { 798 mFileCache = new HashMap<String, FileCacheEntry>(); 799 } else { 800 mFileCache.clear(); 801 } 802 if (mPlayLists == null) { 803 mPlayLists = new ArrayList<FileCacheEntry>(); 804 } else { 805 mPlayLists.clear(); 806 } 807 808 // Build the list of files from the content provider 809 try { 810 // Read existing files from the audio table 811 if (filePath != null) { 812 where = MediaStore.Audio.Media.DATA + "=?"; 813 selectionArgs = new String[] { filePath }; 814 } 815 c = mMediaProvider.query(mAudioUri, AUDIO_PROJECTION, where, selectionArgs, null); 816 817 if (c != null) { 818 try { 819 while (c.moveToNext()) { 820 long rowId = c.getLong(ID_AUDIO_COLUMN_INDEX); 821 String path = c.getString(PATH_AUDIO_COLUMN_INDEX); 822 long lastModified = c.getLong(DATE_MODIFIED_AUDIO_COLUMN_INDEX); 823 824 String key = path; 825 if (mCaseInsensitivePaths) { 826 key = path.toLowerCase(); 827 } 828 mFileCache.put(key, new FileCacheEntry(mAudioUri, rowId, path, 829 lastModified)); 830 } 831 } finally { 832 c.close(); 833 c = null; 834 } 835 } 836 837 // Read existing files from the video table 838 if (filePath != null) { 839 where = MediaStore.Video.Media.DATA + "=?"; 840 } else { 841 where = null; 842 } 843 c = mMediaProvider.query(mVideoUri, VIDEO_PROJECTION, where, selectionArgs, null); 844 845 if (c != null) { 846 try { 847 while (c.moveToNext()) { 848 long rowId = c.getLong(ID_VIDEO_COLUMN_INDEX); 849 String path = c.getString(PATH_VIDEO_COLUMN_INDEX); 850 long lastModified = c.getLong(DATE_MODIFIED_VIDEO_COLUMN_INDEX); 851 852 String key = path; 853 if (mCaseInsensitivePaths) { 854 key = path.toLowerCase(); 855 } 856 mFileCache.put(key, new FileCacheEntry(mVideoUri, rowId, path, 857 lastModified)); 858 } 859 } finally { 860 c.close(); 861 c = null; 862 } 863 } 864 865 // Read existing files from the images table 866 if (filePath != null) { 867 where = MediaStore.Images.Media.DATA + "=?"; 868 } else { 869 where = null; 870 } 871 mOriginalCount = 0; 872 c = mMediaProvider.query(mImagesUri, IMAGES_PROJECTION, where, selectionArgs, null); 873 874 if (c != null) { 875 try { 876 mOriginalCount = c.getCount(); 877 while (c.moveToNext()) { 878 long rowId = c.getLong(ID_IMAGES_COLUMN_INDEX); 879 String path = c.getString(PATH_IMAGES_COLUMN_INDEX); 880 long lastModified = c.getLong(DATE_MODIFIED_IMAGES_COLUMN_INDEX); 881 882 String key = path; 883 if (mCaseInsensitivePaths) { 884 key = path.toLowerCase(); 885 } 886 mFileCache.put(key, new FileCacheEntry(mImagesUri, rowId, path, 887 lastModified)); 888 } 889 } finally { 890 c.close(); 891 c = null; 892 } 893 } 894 895 if (mProcessPlaylists) { 896 // Read existing files from the playlists table 897 if (filePath != null) { 898 where = MediaStore.Audio.Playlists.DATA + "=?"; 899 } else { 900 where = null; 901 } 902 c = mMediaProvider.query(mPlaylistsUri, PLAYLISTS_PROJECTION, where, selectionArgs, null); 903 904 if (c != null) { 905 try { 906 while (c.moveToNext()) { 907 String path = c.getString(PATH_IMAGES_COLUMN_INDEX); 908 909 if (path != null && path.length() > 0) { 910 long rowId = c.getLong(ID_PLAYLISTS_COLUMN_INDEX); 911 long lastModified = c.getLong(DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX); 912 913 String key = path; 914 if (mCaseInsensitivePaths) { 915 key = path.toLowerCase(); 916 } 917 mFileCache.put(key, new FileCacheEntry(mPlaylistsUri, rowId, path, 918 lastModified)); 919 } 920 } 921 } finally { 922 c.close(); 923 c = null; 924 } 925 } 926 } 927 } 928 finally { 929 if (c != null) { 930 c.close(); 931 } 932 } 933 } 934 935 private boolean inScanDirectory(String path, String[] directories) { 936 for (int i = 0; i < directories.length; i++) { 937 if (path.startsWith(directories[i])) { 938 return true; 939 } 940 } 941 return false; 942 } 943 944 private void pruneDeadThumbnailFiles() { 945 HashSet<String> existingFiles = new HashSet<String>(); 946 String directory = "/sdcard/DCIM/.thumbnails"; 947 String [] files = (new File(directory)).list(); 948 if (files == null) 949 files = new String[0]; 950 951 for (int i = 0; i < files.length; i++) { 952 String fullPathString = directory + "/" + files[i]; 953 existingFiles.add(fullPathString); 954 } 955 956 try { 957 Cursor c = mMediaProvider.query( 958 mThumbsUri, 959 new String [] { "_data" }, 960 null, 961 null, 962 null); 963 Log.v(TAG, "pruneDeadThumbnailFiles... " + c); 964 if (c != null && c.moveToFirst()) { 965 do { 966 String fullPathString = c.getString(0); 967 existingFiles.remove(fullPathString); 968 } while (c.moveToNext()); 969 } 970 971 for (String fileToDelete : existingFiles) { 972 if (Config.LOGV) 973 Log.v(TAG, "fileToDelete is " + fileToDelete); 974 try { 975 (new File(fileToDelete)).delete(); 976 } catch (SecurityException ex) { 977 } 978 } 979 980 Log.v(TAG, "/pruneDeadThumbnailFiles... " + c); 981 if (c != null) { 982 c.close(); 983 } 984 } catch (RemoteException e) { 985 // We will soon be killed... 986 } 987 } 988 989 private void postscan(String[] directories) throws RemoteException { 990 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 991 992 while (iterator.hasNext()) { 993 FileCacheEntry entry = iterator.next(); 994 String path = entry.mPath; 995 996 // remove database entries for files that no longer exist. 997 boolean fileMissing = false; 998 999 if (!entry.mSeenInFileSystem) { 1000 if (inScanDirectory(path, directories)) { 1001 // we didn't see this file in the scan directory. 1002 fileMissing = true; 1003 } else { 1004 // the file is outside of our scan directory, 1005 // so we need to check for file existence here. 1006 File testFile = new File(path); 1007 if (!testFile.exists()) { 1008 fileMissing = true; 1009 } 1010 } 1011 } 1012 1013 if (fileMissing) { 1014 // do not delete missing playlists, since they may have been modified by the user. 1015 // the user can delete them in the media player instead. 1016 // instead, clear the path and lastModified fields in the row 1017 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1018 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1019 1020 if (MediaFile.isPlayListFileType(fileType)) { 1021 ContentValues values = new ContentValues(); 1022 values.put(MediaStore.Audio.Playlists.DATA, ""); 1023 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, 0); 1024 mMediaProvider.update(ContentUris.withAppendedId(mPlaylistsUri, entry.mRowId), values, null, null); 1025 } else { 1026 mMediaProvider.delete(ContentUris.withAppendedId(entry.mTableUri, entry.mRowId), null, null); 1027 iterator.remove(); 1028 } 1029 } 1030 } 1031 1032 // handle playlists last, after we know what media files are on the storage. 1033 if (mProcessPlaylists) { 1034 processPlayLists(); 1035 } 1036 1037 if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external"))) 1038 pruneDeadThumbnailFiles(); 1039 1040 // allow GC to clean up 1041 mGenreCache = null; 1042 mPlayLists = null; 1043 mFileCache = null; 1044 mMediaProvider = null; 1045 } 1046 1047 private void initialize(String volumeName) { 1048 mMediaProvider = mContext.getContentResolver().acquireProvider("media"); 1049 1050 mAudioUri = Audio.Media.getContentUri(volumeName); 1051 mVideoUri = Video.Media.getContentUri(volumeName); 1052 mImagesUri = Images.Media.getContentUri(volumeName); 1053 mThumbsUri = Images.Thumbnails.getContentUri(volumeName); 1054 1055 if (!volumeName.equals("internal")) { 1056 // we only support playlists on external media 1057 mProcessPlaylists = true; 1058 mProcessGenres = true; 1059 mGenreCache = new HashMap<String, Uri>(); 1060 mGenresUri = Genres.getContentUri(volumeName); 1061 mPlaylistsUri = Playlists.getContentUri(volumeName); 1062 // assuming external storage is FAT (case insensitive), except on the simulator. 1063 if ( Process.supportsProcesses()) { 1064 mCaseInsensitivePaths = true; 1065 } 1066 } 1067 } 1068 1069 public void scanDirectories(String[] directories, String volumeName) { 1070 try { 1071 long start = System.currentTimeMillis(); 1072 initialize(volumeName); 1073 prescan(null); 1074 long prescan = System.currentTimeMillis(); 1075 1076 for (int i = 0; i < directories.length; i++) { 1077 processDirectory(directories[i], MediaFile.sFileExtensions, mClient); 1078 } 1079 long scan = System.currentTimeMillis(); 1080 postscan(directories); 1081 long end = System.currentTimeMillis(); 1082 1083 if (Config.LOGD) { 1084 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1085 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1086 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1087 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1088 } 1089 } catch (SQLException e) { 1090 // this might happen if the SD card is removed while the media scanner is running 1091 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1092 } catch (UnsupportedOperationException e) { 1093 // this might happen if the SD card is removed while the media scanner is running 1094 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1095 } catch (RemoteException e) { 1096 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1097 } 1098 } 1099 1100 // this function is used to scan a single file 1101 public Uri scanSingleFile(String path, String volumeName, String mimeType) { 1102 try { 1103 initialize(volumeName); 1104 prescan(path); 1105 1106 File file = new File(path); 1107 // always scan the file, so we can return the content://media Uri for existing files 1108 return mClient.doScanFile(path, mimeType, file.lastModified(), file.length(), true); 1109 } catch (RemoteException e) { 1110 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1111 return null; 1112 } 1113 } 1114 1115 // returns the number of matching file/directory names, starting from the right 1116 private int matchPaths(String path1, String path2) { 1117 int result = 0; 1118 int end1 = path1.length(); 1119 int end2 = path2.length(); 1120 1121 while (end1 > 0 && end2 > 0) { 1122 int slash1 = path1.lastIndexOf('/', end1 - 1); 1123 int slash2 = path2.lastIndexOf('/', end2 - 1); 1124 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1125 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1126 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1127 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1128 if (start1 < 0) start1 = 0; else start1++; 1129 if (start2 < 0) start2 = 0; else start2++; 1130 int length = end1 - start1; 1131 if (end2 - start2 != length) break; 1132 if (path1.regionMatches(true, start1, path2, start2, length)) { 1133 result++; 1134 end1 = start1 - 1; 1135 end2 = start2 - 1; 1136 } else break; 1137 } 1138 1139 return result; 1140 } 1141 1142 private boolean addPlayListEntry(String entry, String playListDirectory, 1143 Uri uri, ContentValues values, int index) { 1144 1145 // watch for trailing whitespace 1146 int entryLength = entry.length(); 1147 while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--; 1148 // path should be longer than 3 characters. 1149 // avoid index out of bounds errors below by returning here. 1150 if (entryLength < 3) return false; 1151 if (entryLength < entry.length()) entry = entry.substring(0, entryLength); 1152 1153 // does entry appear to be an absolute path? 1154 // look for Unix or DOS absolute paths 1155 char ch1 = entry.charAt(0); 1156 boolean fullPath = (ch1 == '/' || 1157 (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\')); 1158 // if we have a relative path, combine entry with playListDirectory 1159 if (!fullPath) 1160 entry = playListDirectory + entry; 1161 1162 //FIXME - should we look for "../" within the path? 1163 1164 // best matching MediaFile for the play list entry 1165 FileCacheEntry bestMatch = null; 1166 1167 // number of rightmost file/directory names for bestMatch 1168 int bestMatchLength = 0; 1169 1170 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 1171 while (iterator.hasNext()) { 1172 FileCacheEntry cacheEntry = iterator.next(); 1173 String path = cacheEntry.mPath; 1174 1175 if (path.equalsIgnoreCase(entry)) { 1176 bestMatch = cacheEntry; 1177 break; // don't bother continuing search 1178 } 1179 1180 int matchLength = matchPaths(path, entry); 1181 if (matchLength > bestMatchLength) { 1182 bestMatch = cacheEntry; 1183 bestMatchLength = matchLength; 1184 } 1185 } 1186 1187 if (bestMatch == null) { 1188 return false; 1189 } 1190 1191 try { 1192 // OK, now we need to add this to the database 1193 values.clear(); 1194 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1195 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId)); 1196 mMediaProvider.insert(uri, values); 1197 } catch (RemoteException e) { 1198 Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e); 1199 return false; 1200 } 1201 1202 return true; 1203 } 1204 1205 private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1206 BufferedReader reader = null; 1207 try { 1208 File f = new File(path); 1209 if (f.exists()) { 1210 reader = new BufferedReader( 1211 new InputStreamReader(new FileInputStream(f)), 8192); 1212 String line = reader.readLine(); 1213 int index = 0; 1214 while (line != null) { 1215 // ignore comment lines, which begin with '#' 1216 if (line.length() > 0 && line.charAt(0) != '#') { 1217 values.clear(); 1218 if (addPlayListEntry(line, playListDirectory, uri, values, index)) 1219 index++; 1220 } 1221 line = reader.readLine(); 1222 } 1223 } 1224 } catch (IOException e) { 1225 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1226 } finally { 1227 try { 1228 if (reader != null) 1229 reader.close(); 1230 } catch (IOException e) { 1231 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1232 } 1233 } 1234 } 1235 1236 private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1237 BufferedReader reader = null; 1238 try { 1239 File f = new File(path); 1240 if (f.exists()) { 1241 reader = new BufferedReader( 1242 new InputStreamReader(new FileInputStream(f)), 8192); 1243 String line = reader.readLine(); 1244 int index = 0; 1245 while (line != null) { 1246 // ignore comment lines, which begin with '#' 1247 if (line.startsWith("File")) { 1248 int equals = line.indexOf('='); 1249 if (equals > 0) { 1250 values.clear(); 1251 if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index)) 1252 index++; 1253 } 1254 } 1255 line = reader.readLine(); 1256 } 1257 } 1258 } catch (IOException e) { 1259 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1260 } finally { 1261 try { 1262 if (reader != null) 1263 reader.close(); 1264 } catch (IOException e) { 1265 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1266 } 1267 } 1268 } 1269 1270 class WplHandler implements ElementListener { 1271 1272 final ContentHandler handler; 1273 String playListDirectory; 1274 Uri uri; 1275 ContentValues values = new ContentValues(); 1276 int index = 0; 1277 1278 public WplHandler(String playListDirectory, Uri uri) { 1279 this.playListDirectory = playListDirectory; 1280 this.uri = uri; 1281 1282 RootElement root = new RootElement("smil"); 1283 Element body = root.getChild("body"); 1284 Element seq = body.getChild("seq"); 1285 Element media = seq.getChild("media"); 1286 media.setElementListener(this); 1287 1288 this.handler = root.getContentHandler(); 1289 } 1290 1291 public void start(Attributes attributes) { 1292 String path = attributes.getValue("", "src"); 1293 if (path != null) { 1294 values.clear(); 1295 if (addPlayListEntry(path, playListDirectory, uri, values, index)) { 1296 index++; 1297 } 1298 } 1299 } 1300 1301 public void end() { 1302 } 1303 1304 ContentHandler getContentHandler() { 1305 return handler; 1306 } 1307 } 1308 1309 private void processWplPlayList(String path, String playListDirectory, Uri uri) { 1310 FileInputStream fis = null; 1311 try { 1312 File f = new File(path); 1313 if (f.exists()) { 1314 fis = new FileInputStream(f); 1315 1316 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler()); 1317 } 1318 } catch (SAXException e) { 1319 e.printStackTrace(); 1320 } catch (IOException e) { 1321 e.printStackTrace(); 1322 } finally { 1323 try { 1324 if (fis != null) 1325 fis.close(); 1326 } catch (IOException e) { 1327 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1328 } 1329 } 1330 } 1331 1332 private void processPlayLists() throws RemoteException { 1333 Iterator<FileCacheEntry> iterator = mPlayLists.iterator(); 1334 while (iterator.hasNext()) { 1335 FileCacheEntry entry = iterator.next(); 1336 String path = entry.mPath; 1337 1338 // only process playlist files if they are new or have been modified since the last scan 1339 if (entry.mLastModifiedChanged) { 1340 ContentValues values = new ContentValues(); 1341 int lastSlash = path.lastIndexOf('/'); 1342 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1343 Uri uri, membersUri; 1344 long rowId = entry.mRowId; 1345 if (rowId == 0) { 1346 // Create a new playlist 1347 1348 int lastDot = path.lastIndexOf('.'); 1349 String name = (lastDot < 0 ? path.substring(lastSlash + 1) : path.substring(lastSlash + 1, lastDot)); 1350 values.put(MediaStore.Audio.Playlists.NAME, name); 1351 values.put(MediaStore.Audio.Playlists.DATA, path); 1352 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1353 uri = mMediaProvider.insert(mPlaylistsUri, values); 1354 rowId = ContentUris.parseId(uri); 1355 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1356 } else { 1357 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1358 1359 // update lastModified value of existing playlist 1360 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1361 mMediaProvider.update(uri, values, null, null); 1362 1363 // delete members of existing playlist 1364 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1365 mMediaProvider.delete(membersUri, null, null); 1366 } 1367 1368 String playListDirectory = path.substring(0, lastSlash + 1); 1369 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1370 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1371 1372 if (fileType == MediaFile.FILE_TYPE_M3U) 1373 processM3uPlayList(path, playListDirectory, membersUri, values); 1374 else if (fileType == MediaFile.FILE_TYPE_PLS) 1375 processPlsPlayList(path, playListDirectory, membersUri, values); 1376 else if (fileType == MediaFile.FILE_TYPE_WPL) 1377 processWplPlayList(path, playListDirectory, membersUri); 1378 1379 Cursor cursor = mMediaProvider.query(membersUri, PLAYLIST_MEMBERS_PROJECTION, null, 1380 null, null); 1381 try { 1382 if (cursor == null || cursor.getCount() == 0) { 1383 Log.d(TAG, "playlist is empty - deleting"); 1384 mMediaProvider.delete(uri, null, null); 1385 } 1386 } finally { 1387 if (cursor != null) cursor.close(); 1388 } 1389 } 1390 } 1391 } 1392 1393 private native void processDirectory(String path, String extensions, MediaScannerClient client); 1394 private native void processFile(String path, String mimeType, MediaScannerClient client); 1395 public native void setLocale(String locale); 1396 1397 public native byte[] extractAlbumArt(FileDescriptor fd); 1398 1399 private native final void native_setup(); 1400 private native final void native_finalize(); 1401 @Override 1402 protected void finalize() { 1403 mContext.getContentResolver().releaseProvider(mMediaProvider); 1404 native_finalize(); 1405 } 1406} 1407