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