MediaScanner.java revision ab6ee15f8cd2f34aa5e3f1d84dbd910ce7f49ddd
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 (title == null || TextUtils.isEmpty(title.trim())) { 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 (mFileType == MediaFile.FILE_TYPE_JPEG) { 690 HashMap<String, String> exifData = 691 ExifInterface.loadExifData(entry.mPath); 692 if (exifData != null) { 693 float[] latlng = ExifInterface.getLatLng(exifData); 694 if (latlng != null) { 695 values.put(Images.Media.LATITUDE, latlng[0]); 696 values.put(Images.Media.LONGITUDE, latlng[1]); 697 } 698 } 699 } 700 701 Uri result = null; 702 long rowId = entry.mRowId; 703 if (rowId == 0) { 704 // new file, insert it 705 result = mMediaProvider.insert(tableUri, values); 706 if (result != null) { 707 rowId = ContentUris.parseId(result); 708 entry.mRowId = rowId; 709 } 710 } else { 711 // updated file 712 result = ContentUris.withAppendedId(tableUri, rowId); 713 mMediaProvider.update(result, values, null, null); 714 } 715 if (mProcessGenres && mGenre != null) { 716 String genre = mGenre; 717 Uri uri = mGenreCache.get(genre); 718 if (uri == null) { 719 Cursor cursor = null; 720 try { 721 // see if the genre already exists 722 cursor = mMediaProvider.query( 723 mGenresUri, 724 GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?", 725 new String[] { genre }, null); 726 if (cursor == null || cursor.getCount() == 0) { 727 // genre does not exist, so create the genre in the genre table 728 values.clear(); 729 values.put(MediaStore.Audio.Genres.NAME, genre); 730 uri = mMediaProvider.insert(mGenresUri, values); 731 } else { 732 // genre already exists, so compute its Uri 733 cursor.moveToNext(); 734 uri = ContentUris.withAppendedId(mGenresUri, cursor.getLong(0)); 735 } 736 if (uri != null) { 737 uri = Uri.withAppendedPath(uri, Genres.Members.CONTENT_DIRECTORY); 738 mGenreCache.put(genre, uri); 739 } 740 } finally { 741 // release the cursor if it exists 742 if (cursor != null) { 743 cursor.close(); 744 } 745 } 746 } 747 748 if (uri != null) { 749 // add entry to audio_genre_map 750 values.clear(); 751 values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId)); 752 mMediaProvider.insert(uri, values); 753 } 754 } 755 756 if (notifications && !mDefaultNotificationSet) { 757 if (TextUtils.isEmpty(mDefaultNotificationFilename) || 758 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) { 759 setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId); 760 mDefaultNotificationSet = true; 761 } 762 } else if (ringtones && !mDefaultRingtoneSet) { 763 if (TextUtils.isEmpty(mDefaultRingtoneFilename) || 764 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) { 765 setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId); 766 mDefaultRingtoneSet = true; 767 } 768 } else if (alarms && !mDefaultAlarmSet) { 769 if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) || 770 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) { 771 setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId); 772 mDefaultAlarmSet = true; 773 } 774 } 775 776 return result; 777 } 778 779 private boolean doesPathHaveFilename(String path, String filename) { 780 int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1; 781 int filenameLength = filename.length(); 782 return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) && 783 pathFilenameStart + filenameLength == path.length(); 784 } 785 786 private void setSettingIfNotSet(String settingName, Uri uri, long rowId) { 787 788 String existingSettingValue = Settings.System.getString(mContext.getContentResolver(), 789 settingName); 790 791 if (TextUtils.isEmpty(existingSettingValue)) { 792 // Set the setting to the given URI 793 Settings.System.putString(mContext.getContentResolver(), settingName, 794 ContentUris.withAppendedId(uri, rowId).toString()); 795 } 796 } 797 798 }; // end of anonymous MediaScannerClient instance 799 800 private void prescan(String filePath) throws RemoteException { 801 Cursor c = null; 802 String where = null; 803 String[] selectionArgs = null; 804 805 if (mFileCache == null) { 806 mFileCache = new HashMap<String, FileCacheEntry>(); 807 } else { 808 mFileCache.clear(); 809 } 810 if (mPlayLists == null) { 811 mPlayLists = new ArrayList<FileCacheEntry>(); 812 } else { 813 mPlayLists.clear(); 814 } 815 816 // Build the list of files from the content provider 817 try { 818 // Read existing files from the audio table 819 if (filePath != null) { 820 where = MediaStore.Audio.Media.DATA + "=?"; 821 selectionArgs = new String[] { filePath }; 822 } 823 c = mMediaProvider.query(mAudioUri, AUDIO_PROJECTION, where, selectionArgs, null); 824 825 if (c != null) { 826 try { 827 while (c.moveToNext()) { 828 long rowId = c.getLong(ID_AUDIO_COLUMN_INDEX); 829 String path = c.getString(PATH_AUDIO_COLUMN_INDEX); 830 long lastModified = c.getLong(DATE_MODIFIED_AUDIO_COLUMN_INDEX); 831 832 String key = path; 833 if (mCaseInsensitivePaths) { 834 key = path.toLowerCase(); 835 } 836 mFileCache.put(key, new FileCacheEntry(mAudioUri, rowId, path, 837 lastModified)); 838 } 839 } finally { 840 c.close(); 841 c = null; 842 } 843 } 844 845 // Read existing files from the video table 846 if (filePath != null) { 847 where = MediaStore.Video.Media.DATA + "=?"; 848 } else { 849 where = null; 850 } 851 c = mMediaProvider.query(mVideoUri, VIDEO_PROJECTION, where, selectionArgs, null); 852 853 if (c != null) { 854 try { 855 while (c.moveToNext()) { 856 long rowId = c.getLong(ID_VIDEO_COLUMN_INDEX); 857 String path = c.getString(PATH_VIDEO_COLUMN_INDEX); 858 long lastModified = c.getLong(DATE_MODIFIED_VIDEO_COLUMN_INDEX); 859 860 String key = path; 861 if (mCaseInsensitivePaths) { 862 key = path.toLowerCase(); 863 } 864 mFileCache.put(key, new FileCacheEntry(mVideoUri, rowId, path, 865 lastModified)); 866 } 867 } finally { 868 c.close(); 869 c = null; 870 } 871 } 872 873 // Read existing files from the images table 874 if (filePath != null) { 875 where = MediaStore.Images.Media.DATA + "=?"; 876 } else { 877 where = null; 878 } 879 mOriginalCount = 0; 880 c = mMediaProvider.query(mImagesUri, IMAGES_PROJECTION, where, selectionArgs, null); 881 882 if (c != null) { 883 try { 884 mOriginalCount = c.getCount(); 885 while (c.moveToNext()) { 886 long rowId = c.getLong(ID_IMAGES_COLUMN_INDEX); 887 String path = c.getString(PATH_IMAGES_COLUMN_INDEX); 888 long lastModified = c.getLong(DATE_MODIFIED_IMAGES_COLUMN_INDEX); 889 890 String key = path; 891 if (mCaseInsensitivePaths) { 892 key = path.toLowerCase(); 893 } 894 mFileCache.put(key, new FileCacheEntry(mImagesUri, rowId, path, 895 lastModified)); 896 } 897 } finally { 898 c.close(); 899 c = null; 900 } 901 } 902 903 if (mProcessPlaylists) { 904 // Read existing files from the playlists table 905 if (filePath != null) { 906 where = MediaStore.Audio.Playlists.DATA + "=?"; 907 } else { 908 where = null; 909 } 910 c = mMediaProvider.query(mPlaylistsUri, PLAYLISTS_PROJECTION, where, selectionArgs, null); 911 912 if (c != null) { 913 try { 914 while (c.moveToNext()) { 915 String path = c.getString(PATH_IMAGES_COLUMN_INDEX); 916 917 if (path != null && path.length() > 0) { 918 long rowId = c.getLong(ID_PLAYLISTS_COLUMN_INDEX); 919 long lastModified = c.getLong(DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX); 920 921 String key = path; 922 if (mCaseInsensitivePaths) { 923 key = path.toLowerCase(); 924 } 925 mFileCache.put(key, new FileCacheEntry(mPlaylistsUri, rowId, path, 926 lastModified)); 927 } 928 } 929 } finally { 930 c.close(); 931 c = null; 932 } 933 } 934 } 935 } 936 finally { 937 if (c != null) { 938 c.close(); 939 } 940 } 941 } 942 943 private boolean inScanDirectory(String path, String[] directories) { 944 for (int i = 0; i < directories.length; i++) { 945 if (path.startsWith(directories[i])) { 946 return true; 947 } 948 } 949 return false; 950 } 951 952 private void pruneDeadThumbnailFiles() { 953 HashSet<String> existingFiles = new HashSet<String>(); 954 String directory = "/sdcard/DCIM/.thumbnails"; 955 String [] files = (new File(directory)).list(); 956 if (files == null) 957 files = new String[0]; 958 959 for (int i = 0; i < files.length; i++) { 960 String fullPathString = directory + "/" + files[i]; 961 existingFiles.add(fullPathString); 962 } 963 964 try { 965 Cursor c = mMediaProvider.query( 966 mThumbsUri, 967 new String [] { "_data" }, 968 null, 969 null, 970 null); 971 Log.v(TAG, "pruneDeadThumbnailFiles... " + c); 972 if (c != null && c.moveToFirst()) { 973 do { 974 String fullPathString = c.getString(0); 975 existingFiles.remove(fullPathString); 976 } while (c.moveToNext()); 977 } 978 979 for (String fileToDelete : existingFiles) { 980 if (Config.LOGV) 981 Log.v(TAG, "fileToDelete is " + fileToDelete); 982 try { 983 (new File(fileToDelete)).delete(); 984 } catch (SecurityException ex) { 985 } 986 } 987 988 Log.v(TAG, "/pruneDeadThumbnailFiles... " + c); 989 if (c != null) { 990 c.close(); 991 } 992 } catch (RemoteException e) { 993 // We will soon be killed... 994 } 995 } 996 997 private void postscan(String[] directories) throws RemoteException { 998 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 999 1000 while (iterator.hasNext()) { 1001 FileCacheEntry entry = iterator.next(); 1002 String path = entry.mPath; 1003 1004 // remove database entries for files that no longer exist. 1005 boolean fileMissing = false; 1006 1007 if (!entry.mSeenInFileSystem) { 1008 if (inScanDirectory(path, directories)) { 1009 // we didn't see this file in the scan directory. 1010 fileMissing = true; 1011 } else { 1012 // the file is outside of our scan directory, 1013 // so we need to check for file existence here. 1014 File testFile = new File(path); 1015 if (!testFile.exists()) { 1016 fileMissing = true; 1017 } 1018 } 1019 } 1020 1021 if (fileMissing) { 1022 // do not delete missing playlists, since they may have been modified by the user. 1023 // the user can delete them in the media player instead. 1024 // instead, clear the path and lastModified fields in the row 1025 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1026 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1027 1028 if (MediaFile.isPlayListFileType(fileType)) { 1029 ContentValues values = new ContentValues(); 1030 values.put(MediaStore.Audio.Playlists.DATA, ""); 1031 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, 0); 1032 mMediaProvider.update(ContentUris.withAppendedId(mPlaylistsUri, entry.mRowId), values, null, null); 1033 } else { 1034 mMediaProvider.delete(ContentUris.withAppendedId(entry.mTableUri, entry.mRowId), null, null); 1035 iterator.remove(); 1036 } 1037 } 1038 } 1039 1040 // handle playlists last, after we know what media files are on the storage. 1041 if (mProcessPlaylists) { 1042 processPlayLists(); 1043 } 1044 1045 if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external"))) 1046 pruneDeadThumbnailFiles(); 1047 1048 // allow GC to clean up 1049 mGenreCache = null; 1050 mPlayLists = null; 1051 mFileCache = null; 1052 mMediaProvider = null; 1053 } 1054 1055 private void initialize(String volumeName) { 1056 mMediaProvider = mContext.getContentResolver().acquireProvider("media"); 1057 1058 mAudioUri = Audio.Media.getContentUri(volumeName); 1059 mVideoUri = Video.Media.getContentUri(volumeName); 1060 mImagesUri = Images.Media.getContentUri(volumeName); 1061 mThumbsUri = Images.Thumbnails.getContentUri(volumeName); 1062 1063 if (!volumeName.equals("internal")) { 1064 // we only support playlists on external media 1065 mProcessPlaylists = true; 1066 mProcessGenres = true; 1067 mGenreCache = new HashMap<String, Uri>(); 1068 mGenresUri = Genres.getContentUri(volumeName); 1069 mPlaylistsUri = Playlists.getContentUri(volumeName); 1070 // assuming external storage is FAT (case insensitive), except on the simulator. 1071 if ( Process.supportsProcesses()) { 1072 mCaseInsensitivePaths = true; 1073 } 1074 } 1075 } 1076 1077 public void scanDirectories(String[] directories, String volumeName) { 1078 try { 1079 long start = System.currentTimeMillis(); 1080 initialize(volumeName); 1081 prescan(null); 1082 long prescan = System.currentTimeMillis(); 1083 1084 for (int i = 0; i < directories.length; i++) { 1085 processDirectory(directories[i], MediaFile.sFileExtensions, mClient); 1086 } 1087 long scan = System.currentTimeMillis(); 1088 postscan(directories); 1089 long end = System.currentTimeMillis(); 1090 1091 if (Config.LOGD) { 1092 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1093 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1094 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1095 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1096 } 1097 } catch (SQLException e) { 1098 // this might happen if the SD card is removed while the media scanner is running 1099 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1100 } catch (UnsupportedOperationException e) { 1101 // this might happen if the SD card is removed while the media scanner is running 1102 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1103 } catch (RemoteException e) { 1104 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1105 } 1106 } 1107 1108 // this function is used to scan a single file 1109 public Uri scanSingleFile(String path, String volumeName, String mimeType) { 1110 try { 1111 initialize(volumeName); 1112 prescan(path); 1113 1114 File file = new File(path); 1115 // always scan the file, so we can return the content://media Uri for existing files 1116 return mClient.doScanFile(path, mimeType, file.lastModified(), file.length(), true); 1117 } catch (RemoteException e) { 1118 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1119 return null; 1120 } 1121 } 1122 1123 // returns the number of matching file/directory names, starting from the right 1124 private int matchPaths(String path1, String path2) { 1125 int result = 0; 1126 int end1 = path1.length(); 1127 int end2 = path2.length(); 1128 1129 while (end1 > 0 && end2 > 0) { 1130 int slash1 = path1.lastIndexOf('/', end1 - 1); 1131 int slash2 = path2.lastIndexOf('/', end2 - 1); 1132 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1133 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1134 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1135 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1136 if (start1 < 0) start1 = 0; else start1++; 1137 if (start2 < 0) start2 = 0; else start2++; 1138 int length = end1 - start1; 1139 if (end2 - start2 != length) break; 1140 if (path1.regionMatches(true, start1, path2, start2, length)) { 1141 result++; 1142 end1 = start1 - 1; 1143 end2 = start2 - 1; 1144 } else break; 1145 } 1146 1147 return result; 1148 } 1149 1150 private boolean addPlayListEntry(String entry, String playListDirectory, 1151 Uri uri, ContentValues values, int index) { 1152 1153 // watch for trailing whitespace 1154 int entryLength = entry.length(); 1155 while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--; 1156 // path should be longer than 3 characters. 1157 // avoid index out of bounds errors below by returning here. 1158 if (entryLength < 3) return false; 1159 if (entryLength < entry.length()) entry = entry.substring(0, entryLength); 1160 1161 // does entry appear to be an absolute path? 1162 // look for Unix or DOS absolute paths 1163 char ch1 = entry.charAt(0); 1164 boolean fullPath = (ch1 == '/' || 1165 (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\')); 1166 // if we have a relative path, combine entry with playListDirectory 1167 if (!fullPath) 1168 entry = playListDirectory + entry; 1169 1170 //FIXME - should we look for "../" within the path? 1171 1172 // best matching MediaFile for the play list entry 1173 FileCacheEntry bestMatch = null; 1174 1175 // number of rightmost file/directory names for bestMatch 1176 int bestMatchLength = 0; 1177 1178 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 1179 while (iterator.hasNext()) { 1180 FileCacheEntry cacheEntry = iterator.next(); 1181 String path = cacheEntry.mPath; 1182 1183 if (path.equalsIgnoreCase(entry)) { 1184 bestMatch = cacheEntry; 1185 break; // don't bother continuing search 1186 } 1187 1188 int matchLength = matchPaths(path, entry); 1189 if (matchLength > bestMatchLength) { 1190 bestMatch = cacheEntry; 1191 bestMatchLength = matchLength; 1192 } 1193 } 1194 1195 if (bestMatch == null) { 1196 return false; 1197 } 1198 1199 try { 1200 // OK, now we need to add this to the database 1201 values.clear(); 1202 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1203 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId)); 1204 mMediaProvider.insert(uri, values); 1205 } catch (RemoteException e) { 1206 Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e); 1207 return false; 1208 } 1209 1210 return true; 1211 } 1212 1213 private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1214 BufferedReader reader = null; 1215 try { 1216 File f = new File(path); 1217 if (f.exists()) { 1218 reader = new BufferedReader( 1219 new InputStreamReader(new FileInputStream(f)), 8192); 1220 String line = reader.readLine(); 1221 int index = 0; 1222 while (line != null) { 1223 // ignore comment lines, which begin with '#' 1224 if (line.length() > 0 && line.charAt(0) != '#') { 1225 values.clear(); 1226 if (addPlayListEntry(line, playListDirectory, uri, values, index)) 1227 index++; 1228 } 1229 line = reader.readLine(); 1230 } 1231 } 1232 } catch (IOException e) { 1233 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1234 } finally { 1235 try { 1236 if (reader != null) 1237 reader.close(); 1238 } catch (IOException e) { 1239 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1240 } 1241 } 1242 } 1243 1244 private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1245 BufferedReader reader = null; 1246 try { 1247 File f = new File(path); 1248 if (f.exists()) { 1249 reader = new BufferedReader( 1250 new InputStreamReader(new FileInputStream(f)), 8192); 1251 String line = reader.readLine(); 1252 int index = 0; 1253 while (line != null) { 1254 // ignore comment lines, which begin with '#' 1255 if (line.startsWith("File")) { 1256 int equals = line.indexOf('='); 1257 if (equals > 0) { 1258 values.clear(); 1259 if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index)) 1260 index++; 1261 } 1262 } 1263 line = reader.readLine(); 1264 } 1265 } 1266 } catch (IOException e) { 1267 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1268 } finally { 1269 try { 1270 if (reader != null) 1271 reader.close(); 1272 } catch (IOException e) { 1273 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1274 } 1275 } 1276 } 1277 1278 class WplHandler implements ElementListener { 1279 1280 final ContentHandler handler; 1281 String playListDirectory; 1282 Uri uri; 1283 ContentValues values = new ContentValues(); 1284 int index = 0; 1285 1286 public WplHandler(String playListDirectory, Uri uri) { 1287 this.playListDirectory = playListDirectory; 1288 this.uri = uri; 1289 1290 RootElement root = new RootElement("smil"); 1291 Element body = root.getChild("body"); 1292 Element seq = body.getChild("seq"); 1293 Element media = seq.getChild("media"); 1294 media.setElementListener(this); 1295 1296 this.handler = root.getContentHandler(); 1297 } 1298 1299 public void start(Attributes attributes) { 1300 String path = attributes.getValue("", "src"); 1301 if (path != null) { 1302 values.clear(); 1303 if (addPlayListEntry(path, playListDirectory, uri, values, index)) { 1304 index++; 1305 } 1306 } 1307 } 1308 1309 public void end() { 1310 } 1311 1312 ContentHandler getContentHandler() { 1313 return handler; 1314 } 1315 } 1316 1317 private void processWplPlayList(String path, String playListDirectory, Uri uri) { 1318 FileInputStream fis = null; 1319 try { 1320 File f = new File(path); 1321 if (f.exists()) { 1322 fis = new FileInputStream(f); 1323 1324 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler()); 1325 } 1326 } catch (SAXException e) { 1327 e.printStackTrace(); 1328 } catch (IOException e) { 1329 e.printStackTrace(); 1330 } finally { 1331 try { 1332 if (fis != null) 1333 fis.close(); 1334 } catch (IOException e) { 1335 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1336 } 1337 } 1338 } 1339 1340 private void processPlayLists() throws RemoteException { 1341 Iterator<FileCacheEntry> iterator = mPlayLists.iterator(); 1342 while (iterator.hasNext()) { 1343 FileCacheEntry entry = iterator.next(); 1344 String path = entry.mPath; 1345 1346 // only process playlist files if they are new or have been modified since the last scan 1347 if (entry.mLastModifiedChanged) { 1348 ContentValues values = new ContentValues(); 1349 int lastSlash = path.lastIndexOf('/'); 1350 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1351 Uri uri, membersUri; 1352 long rowId = entry.mRowId; 1353 if (rowId == 0) { 1354 // Create a new playlist 1355 1356 int lastDot = path.lastIndexOf('.'); 1357 String name = (lastDot < 0 ? path.substring(lastSlash + 1) : path.substring(lastSlash + 1, lastDot)); 1358 values.put(MediaStore.Audio.Playlists.NAME, name); 1359 values.put(MediaStore.Audio.Playlists.DATA, path); 1360 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1361 uri = mMediaProvider.insert(mPlaylistsUri, values); 1362 rowId = ContentUris.parseId(uri); 1363 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1364 } else { 1365 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1366 1367 // update lastModified value of existing playlist 1368 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1369 mMediaProvider.update(uri, values, null, null); 1370 1371 // delete members of existing playlist 1372 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1373 mMediaProvider.delete(membersUri, null, null); 1374 } 1375 1376 String playListDirectory = path.substring(0, lastSlash + 1); 1377 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1378 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1379 1380 if (fileType == MediaFile.FILE_TYPE_M3U) 1381 processM3uPlayList(path, playListDirectory, membersUri, values); 1382 else if (fileType == MediaFile.FILE_TYPE_PLS) 1383 processPlsPlayList(path, playListDirectory, membersUri, values); 1384 else if (fileType == MediaFile.FILE_TYPE_WPL) 1385 processWplPlayList(path, playListDirectory, membersUri); 1386 1387 Cursor cursor = mMediaProvider.query(membersUri, PLAYLIST_MEMBERS_PROJECTION, null, 1388 null, null); 1389 try { 1390 if (cursor == null || cursor.getCount() == 0) { 1391 Log.d(TAG, "playlist is empty - deleting"); 1392 mMediaProvider.delete(uri, null, null); 1393 } 1394 } finally { 1395 if (cursor != null) cursor.close(); 1396 } 1397 } 1398 } 1399 } 1400 1401 private native void processDirectory(String path, String extensions, MediaScannerClient client); 1402 private native void processFile(String path, String mimeType, MediaScannerClient client); 1403 public native void setLocale(String locale); 1404 1405 public native byte[] extractAlbumArt(FileDescriptor fd); 1406 1407 private native final void native_setup(); 1408 private native final void native_finalize(); 1409 @Override 1410 protected void finalize() { 1411 mContext.getContentResolver().releaseProvider(mMediaProvider); 1412 native_finalize(); 1413 } 1414} 1415