MediaScanner.java revision ec74abb71f79cdc573c536115acc7ff26a02680a
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 * 1000); 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 * 1000); 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 1207 // lastModified is in milliseconds on Files. 1208 long lastModifiedSeconds = file.lastModified() / 1000; 1209 1210 // always scan the file, so we can return the content://media Uri for existing files 1211 return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(), true); 1212 } catch (RemoteException e) { 1213 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1214 return null; 1215 } 1216 } 1217 1218 // returns the number of matching file/directory names, starting from the right 1219 private int matchPaths(String path1, String path2) { 1220 int result = 0; 1221 int end1 = path1.length(); 1222 int end2 = path2.length(); 1223 1224 while (end1 > 0 && end2 > 0) { 1225 int slash1 = path1.lastIndexOf('/', end1 - 1); 1226 int slash2 = path2.lastIndexOf('/', end2 - 1); 1227 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1228 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1229 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1230 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1231 if (start1 < 0) start1 = 0; else start1++; 1232 if (start2 < 0) start2 = 0; else start2++; 1233 int length = end1 - start1; 1234 if (end2 - start2 != length) break; 1235 if (path1.regionMatches(true, start1, path2, start2, length)) { 1236 result++; 1237 end1 = start1 - 1; 1238 end2 = start2 - 1; 1239 } else break; 1240 } 1241 1242 return result; 1243 } 1244 1245 private boolean addPlayListEntry(String entry, String playListDirectory, 1246 Uri uri, ContentValues values, int index) { 1247 1248 // watch for trailing whitespace 1249 int entryLength = entry.length(); 1250 while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--; 1251 // path should be longer than 3 characters. 1252 // avoid index out of bounds errors below by returning here. 1253 if (entryLength < 3) return false; 1254 if (entryLength < entry.length()) entry = entry.substring(0, entryLength); 1255 1256 // does entry appear to be an absolute path? 1257 // look for Unix or DOS absolute paths 1258 char ch1 = entry.charAt(0); 1259 boolean fullPath = (ch1 == '/' || 1260 (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\')); 1261 // if we have a relative path, combine entry with playListDirectory 1262 if (!fullPath) 1263 entry = playListDirectory + entry; 1264 1265 //FIXME - should we look for "../" within the path? 1266 1267 // best matching MediaFile for the play list entry 1268 FileCacheEntry bestMatch = null; 1269 1270 // number of rightmost file/directory names for bestMatch 1271 int bestMatchLength = 0; 1272 1273 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 1274 while (iterator.hasNext()) { 1275 FileCacheEntry cacheEntry = iterator.next(); 1276 String path = cacheEntry.mPath; 1277 1278 if (path.equalsIgnoreCase(entry)) { 1279 bestMatch = cacheEntry; 1280 break; // don't bother continuing search 1281 } 1282 1283 int matchLength = matchPaths(path, entry); 1284 if (matchLength > bestMatchLength) { 1285 bestMatch = cacheEntry; 1286 bestMatchLength = matchLength; 1287 } 1288 } 1289 1290 // if the match is not for an audio file, bail out 1291 if (bestMatch == null || ! mAudioUri.equals(bestMatch.mTableUri)) { 1292 return false; 1293 } 1294 1295 try { 1296 // OK, now we need to add this to the database 1297 values.clear(); 1298 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1299 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId)); 1300 mMediaProvider.insert(uri, values); 1301 } catch (RemoteException e) { 1302 Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e); 1303 return false; 1304 } 1305 1306 return true; 1307 } 1308 1309 private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1310 BufferedReader reader = null; 1311 try { 1312 File f = new File(path); 1313 if (f.exists()) { 1314 reader = new BufferedReader( 1315 new InputStreamReader(new FileInputStream(f)), 8192); 1316 String line = reader.readLine(); 1317 int index = 0; 1318 while (line != null) { 1319 // ignore comment lines, which begin with '#' 1320 if (line.length() > 0 && line.charAt(0) != '#') { 1321 values.clear(); 1322 if (addPlayListEntry(line, playListDirectory, uri, values, index)) 1323 index++; 1324 } 1325 line = reader.readLine(); 1326 } 1327 } 1328 } catch (IOException e) { 1329 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1330 } finally { 1331 try { 1332 if (reader != null) 1333 reader.close(); 1334 } catch (IOException e) { 1335 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1336 } 1337 } 1338 } 1339 1340 private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1341 BufferedReader reader = null; 1342 try { 1343 File f = new File(path); 1344 if (f.exists()) { 1345 reader = new BufferedReader( 1346 new InputStreamReader(new FileInputStream(f)), 8192); 1347 String line = reader.readLine(); 1348 int index = 0; 1349 while (line != null) { 1350 // ignore comment lines, which begin with '#' 1351 if (line.startsWith("File")) { 1352 int equals = line.indexOf('='); 1353 if (equals > 0) { 1354 values.clear(); 1355 if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index)) 1356 index++; 1357 } 1358 } 1359 line = reader.readLine(); 1360 } 1361 } 1362 } catch (IOException e) { 1363 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1364 } finally { 1365 try { 1366 if (reader != null) 1367 reader.close(); 1368 } catch (IOException e) { 1369 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1370 } 1371 } 1372 } 1373 1374 class WplHandler implements ElementListener { 1375 1376 final ContentHandler handler; 1377 String playListDirectory; 1378 Uri uri; 1379 ContentValues values = new ContentValues(); 1380 int index = 0; 1381 1382 public WplHandler(String playListDirectory, Uri uri) { 1383 this.playListDirectory = playListDirectory; 1384 this.uri = uri; 1385 1386 RootElement root = new RootElement("smil"); 1387 Element body = root.getChild("body"); 1388 Element seq = body.getChild("seq"); 1389 Element media = seq.getChild("media"); 1390 media.setElementListener(this); 1391 1392 this.handler = root.getContentHandler(); 1393 } 1394 1395 public void start(Attributes attributes) { 1396 String path = attributes.getValue("", "src"); 1397 if (path != null) { 1398 values.clear(); 1399 if (addPlayListEntry(path, playListDirectory, uri, values, index)) { 1400 index++; 1401 } 1402 } 1403 } 1404 1405 public void end() { 1406 } 1407 1408 ContentHandler getContentHandler() { 1409 return handler; 1410 } 1411 } 1412 1413 private void processWplPlayList(String path, String playListDirectory, Uri uri) { 1414 FileInputStream fis = null; 1415 try { 1416 File f = new File(path); 1417 if (f.exists()) { 1418 fis = new FileInputStream(f); 1419 1420 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler()); 1421 } 1422 } catch (SAXException e) { 1423 e.printStackTrace(); 1424 } catch (IOException e) { 1425 e.printStackTrace(); 1426 } finally { 1427 try { 1428 if (fis != null) 1429 fis.close(); 1430 } catch (IOException e) { 1431 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1432 } 1433 } 1434 } 1435 1436 private void processPlayLists() throws RemoteException { 1437 Iterator<FileCacheEntry> iterator = mPlayLists.iterator(); 1438 while (iterator.hasNext()) { 1439 FileCacheEntry entry = iterator.next(); 1440 String path = entry.mPath; 1441 1442 // only process playlist files if they are new or have been modified since the last scan 1443 if (entry.mLastModifiedChanged) { 1444 ContentValues values = new ContentValues(); 1445 int lastSlash = path.lastIndexOf('/'); 1446 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1447 Uri uri, membersUri; 1448 long rowId = entry.mRowId; 1449 if (rowId == 0) { 1450 // Create a new playlist 1451 1452 int lastDot = path.lastIndexOf('.'); 1453 String name = (lastDot < 0 ? path.substring(lastSlash + 1) : path.substring(lastSlash + 1, lastDot)); 1454 values.put(MediaStore.Audio.Playlists.NAME, name); 1455 values.put(MediaStore.Audio.Playlists.DATA, path); 1456 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1457 uri = mMediaProvider.insert(mPlaylistsUri, values); 1458 rowId = ContentUris.parseId(uri); 1459 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1460 } else { 1461 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1462 1463 // update lastModified value of existing playlist 1464 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1465 mMediaProvider.update(uri, values, null, null); 1466 1467 // delete members of existing playlist 1468 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1469 mMediaProvider.delete(membersUri, null, null); 1470 } 1471 1472 String playListDirectory = path.substring(0, lastSlash + 1); 1473 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1474 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1475 1476 if (fileType == MediaFile.FILE_TYPE_M3U) 1477 processM3uPlayList(path, playListDirectory, membersUri, values); 1478 else if (fileType == MediaFile.FILE_TYPE_PLS) 1479 processPlsPlayList(path, playListDirectory, membersUri, values); 1480 else if (fileType == MediaFile.FILE_TYPE_WPL) 1481 processWplPlayList(path, playListDirectory, membersUri); 1482 1483 Cursor cursor = mMediaProvider.query(membersUri, PLAYLIST_MEMBERS_PROJECTION, null, 1484 null, null); 1485 try { 1486 if (cursor == null || cursor.getCount() == 0) { 1487 Log.d(TAG, "playlist is empty - deleting"); 1488 mMediaProvider.delete(uri, null, null); 1489 } 1490 } finally { 1491 if (cursor != null) cursor.close(); 1492 } 1493 } 1494 } 1495 } 1496 1497 private native void processDirectory(String path, String extensions, MediaScannerClient client); 1498 private native void processFile(String path, String mimeType, MediaScannerClient client); 1499 public native void setLocale(String locale); 1500 1501 public native byte[] extractAlbumArt(FileDescriptor fd); 1502 1503 private static native final void native_init(); 1504 private native final void native_setup(); 1505 private native final void native_finalize(); 1506 @Override 1507 protected void finalize() { 1508 mContext.getContentResolver().releaseProvider(mMediaProvider); 1509 native_finalize(); 1510 } 1511} 1512