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