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