MediaScanner.java revision c3c08ba5d67d836274fabdf4c1bff6f1053d95a3
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 /** The filename for the default sound for the ringer ringtone. */ 311 private String mDefaultRingtoneFilename; 312 /** The filename for the default sound for the notification ringtone. */ 313 private String mDefaultNotificationFilename; 314 /** 315 * The prefix for system properties that define the default sound for 316 * ringtones. Concatenate the name of the setting from Settings 317 * to get the full system property. 318 */ 319 private static final String DEFAULT_RINGTONE_PROPERTY_PREFIX = "ro.config."; 320 321 // set to true if file path comparisons should be case insensitive. 322 // this should be set when scanning files on a case insensitive file system. 323 private boolean mCaseInsensitivePaths; 324 325 private BitmapFactory.Options mBitmapOptions = new BitmapFactory.Options(); 326 327 private static class FileCacheEntry { 328 Uri mTableUri; 329 long mRowId; 330 String mPath; 331 long mLastModified; 332 boolean mSeenInFileSystem; 333 boolean mLastModifiedChanged; 334 335 FileCacheEntry(Uri tableUri, long rowId, String path, long lastModified) { 336 mTableUri = tableUri; 337 mRowId = rowId; 338 mPath = path; 339 mLastModified = lastModified; 340 mSeenInFileSystem = false; 341 mLastModifiedChanged = false; 342 } 343 344 @Override 345 public String toString() { 346 return mPath; 347 } 348 } 349 350 // hashes file path to FileCacheEntry. 351 // path should be lower case if mCaseInsensitivePaths is true 352 private HashMap<String, FileCacheEntry> mFileCache; 353 354 private ArrayList<FileCacheEntry> mPlayLists; 355 private HashMap<String, Uri> mGenreCache; 356 357 358 public MediaScanner(Context c) { 359 native_setup(); 360 mContext = c; 361 mBitmapOptions.inSampleSize = 1; 362 mBitmapOptions.inJustDecodeBounds = true; 363 364 setDefaultRingtoneFileNames(); 365 } 366 367 private void setDefaultRingtoneFileNames() { 368 mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 369 + Settings.System.RINGTONE); 370 mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 371 + Settings.System.NOTIFICATION_SOUND); 372 } 373 374 private MyMediaScannerClient mClient = new MyMediaScannerClient(); 375 376 private class MyMediaScannerClient implements MediaScannerClient { 377 378 private String mArtist; 379 private String mAlbumArtist; // use this if mArtist is missing 380 private String mAlbum; 381 private String mTitle; 382 private String mComposer; 383 private String mGenre; 384 private String mMimeType; 385 private int mFileType; 386 private int mTrack; 387 private int mYear; 388 private int mDuration; 389 private String mPath; 390 private long mLastModified; 391 private long mFileSize; 392 393 public FileCacheEntry beginFile(String path, String mimeType, long lastModified, long fileSize) { 394 395 // special case certain file names 396 // I use regionMatches() instead of substring() below 397 // to avoid memory allocation 398 int lastSlash = path.lastIndexOf('/'); 399 if (lastSlash >= 0 && lastSlash + 2 < path.length()) { 400 // ignore those ._* files created by MacOS 401 if (path.regionMatches(lastSlash + 1, "._", 0, 2)) { 402 return null; 403 } 404 405 // ignore album art files created by Windows Media Player: 406 // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg and AlbumArt_{...}_Small.jpg 407 if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) { 408 if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) || 409 path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) { 410 return null; 411 } 412 int length = path.length() - lastSlash - 1; 413 if ((length == 17 && path.regionMatches(true, lastSlash + 1, "AlbumArtSmall", 0, 13)) || 414 (length == 10 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) { 415 return null; 416 } 417 } 418 } 419 420 mMimeType = null; 421 // try mimeType first, if it is specified 422 if (mimeType != null) { 423 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 424 if (mFileType != 0) { 425 mMimeType = mimeType; 426 } 427 } 428 mFileSize = fileSize; 429 430 // if mimeType was not specified, compute file type based on file extension. 431 if (mMimeType == null) { 432 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 433 if (mediaFileType != null) { 434 mFileType = mediaFileType.fileType; 435 mMimeType = mediaFileType.mimeType; 436 } 437 } 438 439 String key = path; 440 if (mCaseInsensitivePaths) { 441 key = path.toLowerCase(); 442 } 443 FileCacheEntry entry = mFileCache.get(key); 444 if (entry == null) { 445 entry = new FileCacheEntry(null, 0, path, 0); 446 mFileCache.put(key, entry); 447 } 448 entry.mSeenInFileSystem = true; 449 450 // add some slack to avoid a rounding error 451 long delta = lastModified - entry.mLastModified; 452 if (delta > 1 || delta < -1) { 453 entry.mLastModified = lastModified; 454 entry.mLastModifiedChanged = true; 455 } 456 457 if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) { 458 mPlayLists.add(entry); 459 // we don't process playlists in the main scan, so return null 460 return null; 461 } 462 463 // clear all the metadata 464 mArtist = null; 465 mAlbumArtist = null; 466 mAlbum = null; 467 mTitle = null; 468 mComposer = null; 469 mGenre = null; 470 mTrack = 0; 471 mYear = 0; 472 mDuration = 0; 473 mPath = path; 474 mLastModified = lastModified; 475 476 return entry; 477 } 478 479 public void scanFile(String path, long lastModified, long fileSize) { 480 doScanFile(path, null, lastModified, fileSize, false); 481 } 482 483 public void scanFile(String path, String mimeType, long lastModified, long fileSize) { 484 doScanFile(path, mimeType, lastModified, fileSize, false); 485 } 486 487 public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean scanAlways) { 488 Uri result = null; 489// long t1 = System.currentTimeMillis(); 490 try { 491 FileCacheEntry entry = beginFile(path, mimeType, lastModified, fileSize); 492 // rescan for metadata if file was modified since last scan 493 if (entry != null && (entry.mLastModifiedChanged || scanAlways)) { 494 String lowpath = path.toLowerCase(); 495 boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0); 496 boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0); 497 boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0); 498 boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0); 499 boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) || 500 (!ringtones && !notifications && !alarms && !podcasts); 501 502 if (mFileType == MediaFile.FILE_TYPE_MP3 || 503 mFileType == MediaFile.FILE_TYPE_MP4 || 504 mFileType == MediaFile.FILE_TYPE_M4A || 505 mFileType == MediaFile.FILE_TYPE_3GPP || 506 mFileType == MediaFile.FILE_TYPE_3GPP2 || 507 mFileType == MediaFile.FILE_TYPE_OGG || 508 mFileType == MediaFile.FILE_TYPE_MID || 509 mFileType == MediaFile.FILE_TYPE_WMA) { 510 // we only extract metadata from MP3, M4A, OGG, MID and WMA files. 511 // check MP4 files, to determine if they contain only audio. 512 processFile(path, mimeType, this); 513 } else if (MediaFile.isImageFileType(mFileType)) { 514 // we used to compute the width and height but it's not worth it 515 } 516 517 result = endFile(entry, ringtones, notifications, alarms, music, podcasts); 518 } 519 } catch (RemoteException e) { 520 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 521 } 522// long t2 = System.currentTimeMillis(); 523// Log.v(TAG, "scanFile: " + path + " took " + (t2-t1)); 524 return result; 525 } 526 527 private int parseSubstring(String s, int start, int defaultValue) { 528 int length = s.length(); 529 if (start == length) return defaultValue; 530 531 char ch = s.charAt(start++); 532 // return defaultValue if we have no integer at all 533 if (ch < '0' || ch > '9') return defaultValue; 534 535 int result = ch - '0'; 536 while (start < length) { 537 ch = s.charAt(start++); 538 if (ch < '0' || ch > '9') return result; 539 result = result * 10 + (ch - '0'); 540 } 541 542 return result; 543 } 544 545 public void handleStringTag(String name, String value) { 546 if (name.equalsIgnoreCase("title") || name.startsWith("title;")) { 547 // Don't trim() here, to preserve the special \001 character 548 // used to force sorting. The media provider will trim() before 549 // inserting the title in to the database. 550 mTitle = value; 551 } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) { 552 mArtist = value.trim(); 553 } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")) { 554 mAlbumArtist = value.trim(); 555 } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) { 556 mAlbum = value.trim(); 557 } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) { 558 mComposer = value.trim(); 559 } else if (name.equalsIgnoreCase("genre") || name.startsWith("genre;")) { 560 // handle numeric genres, which PV sometimes encodes like "(20)" 561 if (value.length() > 0) { 562 int genreCode = -1; 563 char ch = value.charAt(0); 564 if (ch == '(') { 565 genreCode = parseSubstring(value, 1, -1); 566 } else if (ch >= '0' && ch <= '9') { 567 genreCode = parseSubstring(value, 0, -1); 568 } 569 if (genreCode >= 0 && genreCode < ID3_GENRES.length) { 570 value = ID3_GENRES[genreCode]; 571 } 572 } 573 mGenre = value; 574 } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) { 575 mYear = parseSubstring(value, 0, 0); 576 } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) { 577 // track number might be of the form "2/12" 578 // we just read the number before the slash 579 int num = parseSubstring(value, 0, 0); 580 mTrack = (mTrack / 1000) * 1000 + num; 581 } else if (name.equalsIgnoreCase("discnumber") || 582 name.equals("set") || name.startsWith("set;")) { 583 // set number might be of the form "1/3" 584 // we just read the number before the slash 585 int num = parseSubstring(value, 0, 0); 586 mTrack = (num * 1000) + (mTrack % 1000); 587 } else if (name.equalsIgnoreCase("duration")) { 588 mDuration = parseSubstring(value, 0, 0); 589 } 590 } 591 592 public void setMimeType(String mimeType) { 593 mMimeType = mimeType; 594 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 595 } 596 597 /** 598 * Formats the data into a values array suitable for use with the Media 599 * Content Provider. 600 * 601 * @return a map of values 602 */ 603 private ContentValues toValues() { 604 ContentValues map = new ContentValues(); 605 606 map.put(MediaStore.MediaColumns.DATA, mPath); 607 map.put(MediaStore.MediaColumns.TITLE, mTitle); 608 map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified); 609 map.put(MediaStore.MediaColumns.SIZE, mFileSize); 610 map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType); 611 612 if (MediaFile.isVideoFileType(mFileType)) { 613 map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaFile.UNKNOWN_STRING)); 614 map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaFile.UNKNOWN_STRING)); 615 map.put(Video.Media.DURATION, mDuration); 616 // FIXME - add RESOLUTION 617 } else if (MediaFile.isImageFileType(mFileType)) { 618 // FIXME - add DESCRIPTION 619 // map.put(field, value); 620 } else if (MediaFile.isAudioFileType(mFileType)) { 621 map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaFile.UNKNOWN_STRING)); 622 map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaFile.UNKNOWN_STRING)); 623 map.put(Audio.Media.COMPOSER, mComposer); 624 if (mYear != 0) { 625 map.put(Audio.Media.YEAR, mYear); 626 } 627 map.put(Audio.Media.TRACK, mTrack); 628 map.put(Audio.Media.DURATION, mDuration); 629 } 630 return map; 631 } 632 633 private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications, 634 boolean alarms, boolean music, boolean podcasts) 635 throws RemoteException { 636 // update database 637 Uri tableUri; 638 boolean isAudio = MediaFile.isAudioFileType(mFileType); 639 boolean isVideo = MediaFile.isVideoFileType(mFileType); 640 boolean isImage = MediaFile.isImageFileType(mFileType); 641 if (isVideo) { 642 tableUri = mVideoUri; 643 } else if (isImage) { 644 tableUri = mImagesUri; 645 } else if (isAudio) { 646 tableUri = mAudioUri; 647 } else { 648 // don't add file to database if not audio, video or image 649 return null; 650 } 651 entry.mTableUri = tableUri; 652 653 // use album artist if artist is missing 654 if (mArtist == null || mArtist.length() == 0) { 655 mArtist = mAlbumArtist; 656 } 657 658 ContentValues values = toValues(); 659 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 660 if (title == null || TextUtils.isEmpty(title.trim())) { 661 title = values.getAsString(MediaStore.MediaColumns.DATA); 662 // extract file name after last slash 663 int lastSlash = title.lastIndexOf('/'); 664 if (lastSlash >= 0) { 665 lastSlash++; 666 if (lastSlash < title.length()) { 667 title = title.substring(lastSlash); 668 } 669 } 670 // truncate the file extension (if any) 671 int lastDot = title.lastIndexOf('.'); 672 if (lastDot > 0) { 673 title = title.substring(0, lastDot); 674 } 675 values.put(MediaStore.MediaColumns.TITLE, title); 676 } 677 String album = values.getAsString(Audio.Media.ALBUM); 678 if (MediaFile.UNKNOWN_STRING.equals(album)) { 679 album = values.getAsString(MediaStore.MediaColumns.DATA); 680 // extract last path segment before file name 681 int lastSlash = album.lastIndexOf('/'); 682 if (lastSlash >= 0) { 683 int previousSlash = 0; 684 while (true) { 685 int idx = album.indexOf('/', previousSlash + 1); 686 if (idx < 0 || idx >= lastSlash) { 687 break; 688 } 689 previousSlash = idx; 690 } 691 if (previousSlash != 0) { 692 album = album.substring(previousSlash + 1, lastSlash); 693 values.put(Audio.Media.ALBUM, album); 694 } 695 } 696 } 697 if (isAudio) { 698 values.put(Audio.Media.IS_RINGTONE, ringtones); 699 values.put(Audio.Media.IS_NOTIFICATION, notifications); 700 values.put(Audio.Media.IS_ALARM, alarms); 701 values.put(Audio.Media.IS_MUSIC, music); 702 values.put(Audio.Media.IS_PODCAST, podcasts); 703 } else if (mFileType == MediaFile.FILE_TYPE_JPEG) { 704 HashMap<String, String> exifData = 705 ExifInterface.loadExifData(entry.mPath); 706 if (exifData != null) { 707 float[] latlng = ExifInterface.getLatLng(exifData); 708 if (latlng != null) { 709 values.put(Images.Media.LATITUDE, latlng[0]); 710 values.put(Images.Media.LONGITUDE, latlng[1]); 711 } 712 } 713 } 714 715 Uri result = null; 716 long rowId = entry.mRowId; 717 if (rowId == 0) { 718 // new file, insert it 719 result = mMediaProvider.insert(tableUri, values); 720 if (result != null) { 721 rowId = ContentUris.parseId(result); 722 entry.mRowId = rowId; 723 } 724 } else { 725 // updated file 726 result = ContentUris.withAppendedId(tableUri, rowId); 727 mMediaProvider.update(result, values, null, null); 728 } 729 if (mProcessGenres && mGenre != null) { 730 String genre = mGenre; 731 Uri uri = mGenreCache.get(genre); 732 if (uri == null) { 733 Cursor cursor = null; 734 try { 735 // see if the genre already exists 736 cursor = mMediaProvider.query( 737 mGenresUri, 738 GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?", 739 new String[] { genre }, null); 740 if (cursor == null || cursor.getCount() == 0) { 741 // genre does not exist, so create the genre in the genre table 742 values.clear(); 743 values.put(MediaStore.Audio.Genres.NAME, genre); 744 uri = mMediaProvider.insert(mGenresUri, values); 745 } else { 746 // genre already exists, so compute its Uri 747 cursor.moveToNext(); 748 uri = ContentUris.withAppendedId(mGenresUri, cursor.getLong(0)); 749 } 750 if (uri != null) { 751 uri = Uri.withAppendedPath(uri, Genres.Members.CONTENT_DIRECTORY); 752 mGenreCache.put(genre, uri); 753 } 754 } finally { 755 // release the cursor if it exists 756 if (cursor != null) { 757 cursor.close(); 758 } 759 } 760 } 761 762 if (uri != null) { 763 // add entry to audio_genre_map 764 values.clear(); 765 values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId)); 766 mMediaProvider.insert(uri, values); 767 } 768 } 769 770 if (notifications && !mDefaultNotificationSet) { 771 if (TextUtils.isEmpty(mDefaultNotificationFilename) || 772 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) { 773 setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId); 774 mDefaultNotificationSet = true; 775 } 776 } else if (ringtones && !mDefaultRingtoneSet) { 777 if (TextUtils.isEmpty(mDefaultRingtoneFilename) || 778 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) { 779 setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId); 780 mDefaultRingtoneSet = true; 781 } 782 } 783 784 return result; 785 } 786 787 private boolean doesPathHaveFilename(String path, String filename) { 788 int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1; 789 int filenameLength = filename.length(); 790 return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) && 791 pathFilenameStart + filenameLength == path.length(); 792 } 793 794 private void setSettingIfNotSet(String settingName, Uri uri, long rowId) { 795 796 String existingSettingValue = Settings.System.getString(mContext.getContentResolver(), 797 settingName); 798 799 if (TextUtils.isEmpty(existingSettingValue)) { 800 // Set the setting to the given URI 801 Settings.System.putString(mContext.getContentResolver(), settingName, 802 ContentUris.withAppendedId(uri, rowId).toString()); 803 } 804 } 805 806 }; // end of anonymous MediaScannerClient instance 807 808 private void prescan(String filePath) throws RemoteException { 809 Cursor c = null; 810 String where = null; 811 String[] selectionArgs = null; 812 813 if (mFileCache == null) { 814 mFileCache = new HashMap<String, FileCacheEntry>(); 815 } else { 816 mFileCache.clear(); 817 } 818 if (mPlayLists == null) { 819 mPlayLists = new ArrayList<FileCacheEntry>(); 820 } else { 821 mPlayLists.clear(); 822 } 823 824 // Build the list of files from the content provider 825 try { 826 // Read existing files from the audio table 827 if (filePath != null) { 828 where = MediaStore.Audio.Media.DATA + "=?"; 829 selectionArgs = new String[] { filePath }; 830 } 831 c = mMediaProvider.query(mAudioUri, AUDIO_PROJECTION, where, selectionArgs, null); 832 833 if (c != null) { 834 try { 835 while (c.moveToNext()) { 836 long rowId = c.getLong(ID_AUDIO_COLUMN_INDEX); 837 String path = c.getString(PATH_AUDIO_COLUMN_INDEX); 838 long lastModified = c.getLong(DATE_MODIFIED_AUDIO_COLUMN_INDEX); 839 840 String key = path; 841 if (mCaseInsensitivePaths) { 842 key = path.toLowerCase(); 843 } 844 mFileCache.put(key, new FileCacheEntry(mAudioUri, rowId, path, 845 lastModified)); 846 } 847 } finally { 848 c.close(); 849 c = null; 850 } 851 } 852 853 // Read existing files from the video table 854 if (filePath != null) { 855 where = MediaStore.Video.Media.DATA + "=?"; 856 } else { 857 where = null; 858 } 859 c = mMediaProvider.query(mVideoUri, VIDEO_PROJECTION, where, selectionArgs, null); 860 861 if (c != null) { 862 try { 863 while (c.moveToNext()) { 864 long rowId = c.getLong(ID_VIDEO_COLUMN_INDEX); 865 String path = c.getString(PATH_VIDEO_COLUMN_INDEX); 866 long lastModified = c.getLong(DATE_MODIFIED_VIDEO_COLUMN_INDEX); 867 868 String key = path; 869 if (mCaseInsensitivePaths) { 870 key = path.toLowerCase(); 871 } 872 mFileCache.put(key, new FileCacheEntry(mVideoUri, rowId, path, 873 lastModified)); 874 } 875 } finally { 876 c.close(); 877 c = null; 878 } 879 } 880 881 // Read existing files from the images table 882 if (filePath != null) { 883 where = MediaStore.Images.Media.DATA + "=?"; 884 } else { 885 where = null; 886 } 887 mOriginalCount = 0; 888 c = mMediaProvider.query(mImagesUri, IMAGES_PROJECTION, where, selectionArgs, null); 889 890 if (c != null) { 891 try { 892 mOriginalCount = c.getCount(); 893 while (c.moveToNext()) { 894 long rowId = c.getLong(ID_IMAGES_COLUMN_INDEX); 895 String path = c.getString(PATH_IMAGES_COLUMN_INDEX); 896 long lastModified = c.getLong(DATE_MODIFIED_IMAGES_COLUMN_INDEX); 897 898 String key = path; 899 if (mCaseInsensitivePaths) { 900 key = path.toLowerCase(); 901 } 902 mFileCache.put(key, new FileCacheEntry(mImagesUri, rowId, path, 903 lastModified)); 904 } 905 } finally { 906 c.close(); 907 c = null; 908 } 909 } 910 911 if (mProcessPlaylists) { 912 // Read existing files from the playlists table 913 if (filePath != null) { 914 where = MediaStore.Audio.Playlists.DATA + "=?"; 915 } else { 916 where = null; 917 } 918 c = mMediaProvider.query(mPlaylistsUri, PLAYLISTS_PROJECTION, where, selectionArgs, null); 919 920 if (c != null) { 921 try { 922 while (c.moveToNext()) { 923 String path = c.getString(PATH_IMAGES_COLUMN_INDEX); 924 925 if (path != null && path.length() > 0) { 926 long rowId = c.getLong(ID_PLAYLISTS_COLUMN_INDEX); 927 long lastModified = c.getLong(DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX); 928 929 String key = path; 930 if (mCaseInsensitivePaths) { 931 key = path.toLowerCase(); 932 } 933 mFileCache.put(key, new FileCacheEntry(mPlaylistsUri, rowId, path, 934 lastModified)); 935 } 936 } 937 } finally { 938 c.close(); 939 c = null; 940 } 941 } 942 } 943 } 944 finally { 945 if (c != null) { 946 c.close(); 947 } 948 } 949 } 950 951 private boolean inScanDirectory(String path, String[] directories) { 952 for (int i = 0; i < directories.length; i++) { 953 if (path.startsWith(directories[i])) { 954 return true; 955 } 956 } 957 return false; 958 } 959 960 private void pruneDeadThumbnailFiles() { 961 HashSet<String> existingFiles = new HashSet<String>(); 962 String directory = "/sdcard/DCIM/.thumbnails"; 963 String [] files = (new File(directory)).list(); 964 if (files == null) 965 files = new String[0]; 966 967 for (int i = 0; i < files.length; i++) { 968 String fullPathString = directory + "/" + files[i]; 969 existingFiles.add(fullPathString); 970 } 971 972 try { 973 Cursor c = mMediaProvider.query( 974 mThumbsUri, 975 new String [] { "_data" }, 976 null, 977 null, 978 null); 979 Log.v(TAG, "pruneDeadThumbnailFiles... " + c); 980 if (c != null && c.moveToFirst()) { 981 do { 982 String fullPathString = c.getString(0); 983 existingFiles.remove(fullPathString); 984 } while (c.moveToNext()); 985 } 986 987 for (String fileToDelete : existingFiles) { 988 if (Config.LOGV) 989 Log.v(TAG, "fileToDelete is " + fileToDelete); 990 try { 991 (new File(fileToDelete)).delete(); 992 } catch (SecurityException ex) { 993 } 994 } 995 996 Log.v(TAG, "/pruneDeadThumbnailFiles... " + c); 997 if (c != null) { 998 c.close(); 999 } 1000 } catch (RemoteException e) { 1001 // We will soon be killed... 1002 } 1003 } 1004 1005 private void postscan(String[] directories) throws RemoteException { 1006 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 1007 1008 while (iterator.hasNext()) { 1009 FileCacheEntry entry = iterator.next(); 1010 String path = entry.mPath; 1011 1012 // remove database entries for files that no longer exist. 1013 boolean fileMissing = false; 1014 1015 if (!entry.mSeenInFileSystem) { 1016 if (inScanDirectory(path, directories)) { 1017 // we didn't see this file in the scan directory. 1018 fileMissing = true; 1019 } else { 1020 // the file is outside of our scan directory, 1021 // so we need to check for file existence here. 1022 File testFile = new File(path); 1023 if (!testFile.exists()) { 1024 fileMissing = true; 1025 } 1026 } 1027 } 1028 1029 if (fileMissing) { 1030 // do not delete missing playlists, since they may have been modified by the user. 1031 // the user can delete them in the media player instead. 1032 // instead, clear the path and lastModified fields in the row 1033 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1034 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1035 1036 if (MediaFile.isPlayListFileType(fileType)) { 1037 ContentValues values = new ContentValues(); 1038 values.put(MediaStore.Audio.Playlists.DATA, ""); 1039 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, 0); 1040 mMediaProvider.update(ContentUris.withAppendedId(mPlaylistsUri, entry.mRowId), values, null, null); 1041 } else { 1042 mMediaProvider.delete(ContentUris.withAppendedId(entry.mTableUri, entry.mRowId), null, null); 1043 iterator.remove(); 1044 } 1045 } 1046 } 1047 1048 // handle playlists last, after we know what media files are on the storage. 1049 if (mProcessPlaylists) { 1050 processPlayLists(); 1051 } 1052 1053 if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external"))) 1054 pruneDeadThumbnailFiles(); 1055 1056 // allow GC to clean up 1057 mGenreCache = null; 1058 mPlayLists = null; 1059 mFileCache = null; 1060 mMediaProvider = null; 1061 } 1062 1063 private void initialize(String volumeName) { 1064 mMediaProvider = mContext.getContentResolver().acquireProvider("media"); 1065 1066 mAudioUri = Audio.Media.getContentUri(volumeName); 1067 mVideoUri = Video.Media.getContentUri(volumeName); 1068 mImagesUri = Images.Media.getContentUri(volumeName); 1069 mThumbsUri = Images.Thumbnails.getContentUri(volumeName); 1070 1071 if (!volumeName.equals("internal")) { 1072 // we only support playlists on external media 1073 mProcessPlaylists = true; 1074 mProcessGenres = true; 1075 mGenreCache = new HashMap<String, Uri>(); 1076 mGenresUri = Genres.getContentUri(volumeName); 1077 mPlaylistsUri = Playlists.getContentUri(volumeName); 1078 // assuming external storage is FAT (case insensitive), except on the simulator. 1079 if ( Process.supportsProcesses()) { 1080 mCaseInsensitivePaths = true; 1081 } 1082 } 1083 } 1084 1085 public void scanDirectories(String[] directories, String volumeName) { 1086 try { 1087 long start = System.currentTimeMillis(); 1088 initialize(volumeName); 1089 prescan(null); 1090 long prescan = System.currentTimeMillis(); 1091 1092 for (int i = 0; i < directories.length; i++) { 1093 processDirectory(directories[i], MediaFile.sFileExtensions, mClient); 1094 } 1095 long scan = System.currentTimeMillis(); 1096 postscan(directories); 1097 long end = System.currentTimeMillis(); 1098 1099 if (Config.LOGD) { 1100 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1101 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1102 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1103 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1104 } 1105 } catch (SQLException e) { 1106 // this might happen if the SD card is removed while the media scanner is running 1107 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1108 } catch (UnsupportedOperationException e) { 1109 // this might happen if the SD card is removed while the media scanner is running 1110 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1111 } catch (RemoteException e) { 1112 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1113 } 1114 } 1115 1116 // this function is used to scan a single file 1117 public Uri scanSingleFile(String path, String volumeName, String mimeType) { 1118 try { 1119 initialize(volumeName); 1120 prescan(path); 1121 1122 File file = new File(path); 1123 // always scan the file, so we can return the content://media Uri for existing files 1124 return mClient.doScanFile(path, mimeType, file.lastModified(), file.length(), true); 1125 } catch (RemoteException e) { 1126 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1127 return null; 1128 } 1129 } 1130 1131 // returns the number of matching file/directory names, starting from the right 1132 private int matchPaths(String path1, String path2) { 1133 int result = 0; 1134 int end1 = path1.length(); 1135 int end2 = path2.length(); 1136 1137 while (end1 > 0 && end2 > 0) { 1138 int slash1 = path1.lastIndexOf('/', end1 - 1); 1139 int slash2 = path2.lastIndexOf('/', end2 - 1); 1140 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1141 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1142 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1143 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1144 if (start1 < 0) start1 = 0; else start1++; 1145 if (start2 < 0) start2 = 0; else start2++; 1146 int length = end1 - start1; 1147 if (end2 - start2 != length) break; 1148 if (path1.regionMatches(true, start1, path2, start2, length)) { 1149 result++; 1150 end1 = start1 - 1; 1151 end2 = start2 - 1; 1152 } else break; 1153 } 1154 1155 return result; 1156 } 1157 1158 private boolean addPlayListEntry(String entry, String playListDirectory, 1159 Uri uri, ContentValues values, int index) { 1160 1161 // watch for trailing whitespace 1162 int entryLength = entry.length(); 1163 while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--; 1164 // path should be longer than 3 characters. 1165 // avoid index out of bounds errors below by returning here. 1166 if (entryLength < 3) return false; 1167 if (entryLength < entry.length()) entry = entry.substring(0, entryLength); 1168 1169 // does entry appear to be an absolute path? 1170 // look for Unix or DOS absolute paths 1171 char ch1 = entry.charAt(0); 1172 boolean fullPath = (ch1 == '/' || 1173 (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\')); 1174 // if we have a relative path, combine entry with playListDirectory 1175 if (!fullPath) 1176 entry = playListDirectory + entry; 1177 1178 //FIXME - should we look for "../" within the path? 1179 1180 // best matching MediaFile for the play list entry 1181 FileCacheEntry bestMatch = null; 1182 1183 // number of rightmost file/directory names for bestMatch 1184 int bestMatchLength = 0; 1185 1186 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 1187 while (iterator.hasNext()) { 1188 FileCacheEntry cacheEntry = iterator.next(); 1189 String path = cacheEntry.mPath; 1190 1191 if (path.equalsIgnoreCase(entry)) { 1192 bestMatch = cacheEntry; 1193 break; // don't bother continuing search 1194 } 1195 1196 int matchLength = matchPaths(path, entry); 1197 if (matchLength > bestMatchLength) { 1198 bestMatch = cacheEntry; 1199 bestMatchLength = matchLength; 1200 } 1201 } 1202 1203 if (bestMatch == null) { 1204 return false; 1205 } 1206 1207 try { 1208 // OK, now we need to add this to the database 1209 values.clear(); 1210 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1211 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId)); 1212 mMediaProvider.insert(uri, values); 1213 } catch (RemoteException e) { 1214 Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e); 1215 return false; 1216 } 1217 1218 return true; 1219 } 1220 1221 private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1222 BufferedReader reader = null; 1223 try { 1224 File f = new File(path); 1225 if (f.exists()) { 1226 reader = new BufferedReader( 1227 new InputStreamReader(new FileInputStream(f)), 8192); 1228 String line = reader.readLine(); 1229 int index = 0; 1230 while (line != null) { 1231 // ignore comment lines, which begin with '#' 1232 if (line.length() > 0 && line.charAt(0) != '#') { 1233 values.clear(); 1234 if (addPlayListEntry(line, playListDirectory, uri, values, index)) 1235 index++; 1236 } 1237 line = reader.readLine(); 1238 } 1239 } 1240 } catch (IOException e) { 1241 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1242 } finally { 1243 try { 1244 if (reader != null) 1245 reader.close(); 1246 } catch (IOException e) { 1247 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1248 } 1249 } 1250 } 1251 1252 private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1253 BufferedReader reader = null; 1254 try { 1255 File f = new File(path); 1256 if (f.exists()) { 1257 reader = new BufferedReader( 1258 new InputStreamReader(new FileInputStream(f)), 8192); 1259 String line = reader.readLine(); 1260 int index = 0; 1261 while (line != null) { 1262 // ignore comment lines, which begin with '#' 1263 if (line.startsWith("File")) { 1264 int equals = line.indexOf('='); 1265 if (equals > 0) { 1266 values.clear(); 1267 if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index)) 1268 index++; 1269 } 1270 } 1271 line = reader.readLine(); 1272 } 1273 } 1274 } catch (IOException e) { 1275 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1276 } finally { 1277 try { 1278 if (reader != null) 1279 reader.close(); 1280 } catch (IOException e) { 1281 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1282 } 1283 } 1284 } 1285 1286 class WplHandler implements ElementListener { 1287 1288 final ContentHandler handler; 1289 String playListDirectory; 1290 Uri uri; 1291 ContentValues values = new ContentValues(); 1292 int index = 0; 1293 1294 public WplHandler(String playListDirectory, Uri uri) { 1295 this.playListDirectory = playListDirectory; 1296 this.uri = uri; 1297 1298 RootElement root = new RootElement("smil"); 1299 Element body = root.getChild("body"); 1300 Element seq = body.getChild("seq"); 1301 Element media = seq.getChild("media"); 1302 media.setElementListener(this); 1303 1304 this.handler = root.getContentHandler(); 1305 } 1306 1307 public void start(Attributes attributes) { 1308 String path = attributes.getValue("", "src"); 1309 if (path != null) { 1310 values.clear(); 1311 if (addPlayListEntry(path, playListDirectory, uri, values, index)) { 1312 index++; 1313 } 1314 } 1315 } 1316 1317 public void end() { 1318 } 1319 1320 ContentHandler getContentHandler() { 1321 return handler; 1322 } 1323 } 1324 1325 private void processWplPlayList(String path, String playListDirectory, Uri uri) { 1326 FileInputStream fis = null; 1327 try { 1328 File f = new File(path); 1329 if (f.exists()) { 1330 fis = new FileInputStream(f); 1331 1332 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler()); 1333 } 1334 } catch (SAXException e) { 1335 e.printStackTrace(); 1336 } catch (IOException e) { 1337 e.printStackTrace(); 1338 } finally { 1339 try { 1340 if (fis != null) 1341 fis.close(); 1342 } catch (IOException e) { 1343 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1344 } 1345 } 1346 } 1347 1348 private void processPlayLists() throws RemoteException { 1349 Iterator<FileCacheEntry> iterator = mPlayLists.iterator(); 1350 while (iterator.hasNext()) { 1351 FileCacheEntry entry = iterator.next(); 1352 String path = entry.mPath; 1353 1354 // only process playlist files if they are new or have been modified since the last scan 1355 if (entry.mLastModifiedChanged) { 1356 ContentValues values = new ContentValues(); 1357 int lastSlash = path.lastIndexOf('/'); 1358 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1359 Uri uri, membersUri; 1360 long rowId = entry.mRowId; 1361 if (rowId == 0) { 1362 // Create a new playlist 1363 1364 int lastDot = path.lastIndexOf('.'); 1365 String name = (lastDot < 0 ? path.substring(lastSlash + 1) : path.substring(lastSlash + 1, lastDot)); 1366 values.put(MediaStore.Audio.Playlists.NAME, name); 1367 values.put(MediaStore.Audio.Playlists.DATA, path); 1368 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1369 uri = mMediaProvider.insert(mPlaylistsUri, values); 1370 rowId = ContentUris.parseId(uri); 1371 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1372 } else { 1373 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1374 1375 // update lastModified value of existing playlist 1376 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1377 mMediaProvider.update(uri, values, null, null); 1378 1379 // delete members of existing playlist 1380 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1381 mMediaProvider.delete(membersUri, null, null); 1382 } 1383 1384 String playListDirectory = path.substring(0, lastSlash + 1); 1385 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1386 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1387 1388 if (fileType == MediaFile.FILE_TYPE_M3U) 1389 processM3uPlayList(path, playListDirectory, membersUri, values); 1390 else if (fileType == MediaFile.FILE_TYPE_PLS) 1391 processPlsPlayList(path, playListDirectory, membersUri, values); 1392 else if (fileType == MediaFile.FILE_TYPE_WPL) 1393 processWplPlayList(path, playListDirectory, membersUri); 1394 1395 Cursor cursor = mMediaProvider.query(membersUri, PLAYLIST_MEMBERS_PROJECTION, null, 1396 null, null); 1397 try { 1398 if (cursor == null || cursor.getCount() == 0) { 1399 Log.d(TAG, "playlist is empty - deleting"); 1400 mMediaProvider.delete(uri, null, null); 1401 } 1402 } finally { 1403 if (cursor != null) cursor.close(); 1404 } 1405 } 1406 } 1407 } 1408 1409 private native void processDirectory(String path, String extensions, MediaScannerClient client); 1410 private native void processFile(String path, String mimeType, MediaScannerClient client); 1411 public native void setLocale(String locale); 1412 1413 public native byte[] extractAlbumArt(FileDescriptor fd); 1414 1415 private native final void native_setup(); 1416 private native final void native_finalize(); 1417 @Override 1418 protected void finalize() { 1419 mContext.getContentResolver().releaseProvider(mMediaProvider); 1420 native_finalize(); 1421 } 1422} 1423