MediaScanner.java revision 35b002a9c873fb40116179c8626b9841b0abae74
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 : MediaStore.UNKNOWN_STRING)); 641 map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaStore.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 : MediaStore.UNKNOWN_STRING)); 652 map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaStore.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 (MediaStore.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 // Only consider entries with absolute path names. 924 // This allows storing URIs in the database without the 925 // media scanner removing them. 926 if (path.startsWith("/")) { 927 String key = path; 928 if (mCaseInsensitivePaths) { 929 key = path.toLowerCase(); 930 } 931 mFileCache.put(key, new FileCacheEntry(mAudioUri, rowId, path, 932 lastModified)); 933 } 934 } 935 } finally { 936 c.close(); 937 c = null; 938 } 939 } 940 941 // Read existing files from the video table 942 if (filePath != null) { 943 where = MediaStore.Video.Media.DATA + "=?"; 944 } else { 945 where = null; 946 } 947 c = mMediaProvider.query(mVideoUri, VIDEO_PROJECTION, where, selectionArgs, null); 948 949 if (c != null) { 950 try { 951 while (c.moveToNext()) { 952 long rowId = c.getLong(ID_VIDEO_COLUMN_INDEX); 953 String path = c.getString(PATH_VIDEO_COLUMN_INDEX); 954 long lastModified = c.getLong(DATE_MODIFIED_VIDEO_COLUMN_INDEX); 955 956 // Only consider entries with absolute path names. 957 // This allows storing URIs in the database without the 958 // media scanner removing them. 959 if (path.startsWith("/")) { 960 String key = path; 961 if (mCaseInsensitivePaths) { 962 key = path.toLowerCase(); 963 } 964 mFileCache.put(key, new FileCacheEntry(mVideoUri, rowId, path, 965 lastModified)); 966 } 967 } 968 } finally { 969 c.close(); 970 c = null; 971 } 972 } 973 974 // Read existing files from the images table 975 if (filePath != null) { 976 where = MediaStore.Images.Media.DATA + "=?"; 977 } else { 978 where = null; 979 } 980 mOriginalCount = 0; 981 c = mMediaProvider.query(mImagesUri, IMAGES_PROJECTION, where, selectionArgs, null); 982 983 if (c != null) { 984 try { 985 mOriginalCount = c.getCount(); 986 while (c.moveToNext()) { 987 long rowId = c.getLong(ID_IMAGES_COLUMN_INDEX); 988 String path = c.getString(PATH_IMAGES_COLUMN_INDEX); 989 long lastModified = c.getLong(DATE_MODIFIED_IMAGES_COLUMN_INDEX); 990 991 // Only consider entries with absolute path names. 992 // This allows storing URIs in the database without the 993 // media scanner removing them. 994 if (path.startsWith("/")) { 995 String key = path; 996 if (mCaseInsensitivePaths) { 997 key = path.toLowerCase(); 998 } 999 mFileCache.put(key, new FileCacheEntry(mImagesUri, rowId, path, 1000 lastModified)); 1001 } 1002 } 1003 } finally { 1004 c.close(); 1005 c = null; 1006 } 1007 } 1008 1009 if (mProcessPlaylists) { 1010 // Read existing files from the playlists table 1011 if (filePath != null) { 1012 where = MediaStore.Audio.Playlists.DATA + "=?"; 1013 } else { 1014 where = null; 1015 } 1016 c = mMediaProvider.query(mPlaylistsUri, PLAYLISTS_PROJECTION, where, selectionArgs, null); 1017 1018 if (c != null) { 1019 try { 1020 while (c.moveToNext()) { 1021 String path = c.getString(PATH_PLAYLISTS_COLUMN_INDEX); 1022 1023 if (path != null && path.length() > 0) { 1024 long rowId = c.getLong(ID_PLAYLISTS_COLUMN_INDEX); 1025 long lastModified = c.getLong(DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX); 1026 1027 String key = path; 1028 if (mCaseInsensitivePaths) { 1029 key = path.toLowerCase(); 1030 } 1031 mFileCache.put(key, new FileCacheEntry(mPlaylistsUri, rowId, path, 1032 lastModified)); 1033 } 1034 } 1035 } finally { 1036 c.close(); 1037 c = null; 1038 } 1039 } 1040 } 1041 } 1042 finally { 1043 if (c != null) { 1044 c.close(); 1045 } 1046 } 1047 } 1048 1049 private boolean inScanDirectory(String path, String[] directories) { 1050 for (int i = 0; i < directories.length; i++) { 1051 if (path.startsWith(directories[i])) { 1052 return true; 1053 } 1054 } 1055 return false; 1056 } 1057 1058 private void pruneDeadThumbnailFiles() { 1059 HashSet<String> existingFiles = new HashSet<String>(); 1060 String directory = "/sdcard/DCIM/.thumbnails"; 1061 String [] files = (new File(directory)).list(); 1062 if (files == null) 1063 files = new String[0]; 1064 1065 for (int i = 0; i < files.length; i++) { 1066 String fullPathString = directory + "/" + files[i]; 1067 existingFiles.add(fullPathString); 1068 } 1069 1070 try { 1071 Cursor c = mMediaProvider.query( 1072 mThumbsUri, 1073 new String [] { "_data" }, 1074 null, 1075 null, 1076 null); 1077 Log.v(TAG, "pruneDeadThumbnailFiles... " + c); 1078 if (c != null && c.moveToFirst()) { 1079 do { 1080 String fullPathString = c.getString(0); 1081 existingFiles.remove(fullPathString); 1082 } while (c.moveToNext()); 1083 } 1084 1085 for (String fileToDelete : existingFiles) { 1086 if (Config.LOGV) 1087 Log.v(TAG, "fileToDelete is " + fileToDelete); 1088 try { 1089 (new File(fileToDelete)).delete(); 1090 } catch (SecurityException ex) { 1091 } 1092 } 1093 1094 Log.v(TAG, "/pruneDeadThumbnailFiles... " + c); 1095 if (c != null) { 1096 c.close(); 1097 } 1098 } catch (RemoteException e) { 1099 // We will soon be killed... 1100 } 1101 } 1102 1103 private void postscan(String[] directories) throws RemoteException { 1104 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 1105 1106 while (iterator.hasNext()) { 1107 FileCacheEntry entry = iterator.next(); 1108 String path = entry.mPath; 1109 1110 // remove database entries for files that no longer exist. 1111 boolean fileMissing = false; 1112 1113 if (!entry.mSeenInFileSystem) { 1114 if (inScanDirectory(path, directories)) { 1115 // we didn't see this file in the scan directory. 1116 fileMissing = true; 1117 } else { 1118 // the file is outside of our scan directory, 1119 // so we need to check for file existence here. 1120 File testFile = new File(path); 1121 if (!testFile.exists()) { 1122 fileMissing = true; 1123 } 1124 } 1125 } 1126 1127 if (fileMissing) { 1128 // do not delete missing playlists, since they may have been modified by the user. 1129 // the user can delete them in the media player instead. 1130 // instead, clear the path and lastModified fields in the row 1131 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1132 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1133 1134 if (MediaFile.isPlayListFileType(fileType)) { 1135 ContentValues values = new ContentValues(); 1136 values.put(MediaStore.Audio.Playlists.DATA, ""); 1137 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, 0); 1138 mMediaProvider.update(ContentUris.withAppendedId(mPlaylistsUri, entry.mRowId), values, null, null); 1139 } else { 1140 mMediaProvider.delete(ContentUris.withAppendedId(entry.mTableUri, entry.mRowId), null, null); 1141 iterator.remove(); 1142 } 1143 } 1144 } 1145 1146 // handle playlists last, after we know what media files are on the storage. 1147 if (mProcessPlaylists) { 1148 processPlayLists(); 1149 } 1150 1151 if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external"))) 1152 pruneDeadThumbnailFiles(); 1153 1154 // allow GC to clean up 1155 mGenreCache = null; 1156 mPlayLists = null; 1157 mFileCache = null; 1158 mMediaProvider = null; 1159 } 1160 1161 private void initialize(String volumeName) { 1162 mMediaProvider = mContext.getContentResolver().acquireProvider("media"); 1163 1164 mAudioUri = Audio.Media.getContentUri(volumeName); 1165 mVideoUri = Video.Media.getContentUri(volumeName); 1166 mImagesUri = Images.Media.getContentUri(volumeName); 1167 mThumbsUri = Images.Thumbnails.getContentUri(volumeName); 1168 1169 if (!volumeName.equals("internal")) { 1170 // we only support playlists on external media 1171 mProcessPlaylists = true; 1172 mProcessGenres = true; 1173 mGenreCache = new HashMap<String, Uri>(); 1174 mGenresUri = Genres.getContentUri(volumeName); 1175 mPlaylistsUri = Playlists.getContentUri(volumeName); 1176 // assuming external storage is FAT (case insensitive), except on the simulator. 1177 if ( Process.supportsProcesses()) { 1178 mCaseInsensitivePaths = true; 1179 } 1180 } 1181 } 1182 1183 public void scanDirectories(String[] directories, String volumeName) { 1184 try { 1185 long start = System.currentTimeMillis(); 1186 initialize(volumeName); 1187 prescan(null); 1188 long prescan = System.currentTimeMillis(); 1189 1190 for (int i = 0; i < directories.length; i++) { 1191 processDirectory(directories[i], MediaFile.sFileExtensions, mClient); 1192 } 1193 long scan = System.currentTimeMillis(); 1194 postscan(directories); 1195 long end = System.currentTimeMillis(); 1196 1197 if (Config.LOGD) { 1198 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1199 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1200 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1201 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1202 } 1203 } catch (SQLException e) { 1204 // this might happen if the SD card is removed while the media scanner is running 1205 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1206 } catch (UnsupportedOperationException e) { 1207 // this might happen if the SD card is removed while the media scanner is running 1208 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1209 } catch (RemoteException e) { 1210 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1211 } 1212 } 1213 1214 // this function is used to scan a single file 1215 public Uri scanSingleFile(String path, String volumeName, String mimeType) { 1216 try { 1217 initialize(volumeName); 1218 prescan(path); 1219 1220 File file = new File(path); 1221 // always scan the file, so we can return the content://media Uri for existing files 1222 return mClient.doScanFile(path, mimeType, file.lastModified(), file.length(), true); 1223 } catch (RemoteException e) { 1224 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1225 return null; 1226 } 1227 } 1228 1229 // returns the number of matching file/directory names, starting from the right 1230 private int matchPaths(String path1, String path2) { 1231 int result = 0; 1232 int end1 = path1.length(); 1233 int end2 = path2.length(); 1234 1235 while (end1 > 0 && end2 > 0) { 1236 int slash1 = path1.lastIndexOf('/', end1 - 1); 1237 int slash2 = path2.lastIndexOf('/', end2 - 1); 1238 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1239 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1240 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1241 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1242 if (start1 < 0) start1 = 0; else start1++; 1243 if (start2 < 0) start2 = 0; else start2++; 1244 int length = end1 - start1; 1245 if (end2 - start2 != length) break; 1246 if (path1.regionMatches(true, start1, path2, start2, length)) { 1247 result++; 1248 end1 = start1 - 1; 1249 end2 = start2 - 1; 1250 } else break; 1251 } 1252 1253 return result; 1254 } 1255 1256 private boolean addPlayListEntry(String entry, String playListDirectory, 1257 Uri uri, ContentValues values, int index) { 1258 1259 // watch for trailing whitespace 1260 int entryLength = entry.length(); 1261 while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--; 1262 // path should be longer than 3 characters. 1263 // avoid index out of bounds errors below by returning here. 1264 if (entryLength < 3) return false; 1265 if (entryLength < entry.length()) entry = entry.substring(0, entryLength); 1266 1267 // does entry appear to be an absolute path? 1268 // look for Unix or DOS absolute paths 1269 char ch1 = entry.charAt(0); 1270 boolean fullPath = (ch1 == '/' || 1271 (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\')); 1272 // if we have a relative path, combine entry with playListDirectory 1273 if (!fullPath) 1274 entry = playListDirectory + entry; 1275 1276 //FIXME - should we look for "../" within the path? 1277 1278 // best matching MediaFile for the play list entry 1279 FileCacheEntry bestMatch = null; 1280 1281 // number of rightmost file/directory names for bestMatch 1282 int bestMatchLength = 0; 1283 1284 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 1285 while (iterator.hasNext()) { 1286 FileCacheEntry cacheEntry = iterator.next(); 1287 String path = cacheEntry.mPath; 1288 1289 if (path.equalsIgnoreCase(entry)) { 1290 bestMatch = cacheEntry; 1291 break; // don't bother continuing search 1292 } 1293 1294 int matchLength = matchPaths(path, entry); 1295 if (matchLength > bestMatchLength) { 1296 bestMatch = cacheEntry; 1297 bestMatchLength = matchLength; 1298 } 1299 } 1300 1301 // if the match is not for an audio file, bail out 1302 if (bestMatch == null || ! mAudioUri.equals(bestMatch.mTableUri)) { 1303 return false; 1304 } 1305 1306 try { 1307 // OK, now we need to add this to the database 1308 values.clear(); 1309 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1310 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId)); 1311 mMediaProvider.insert(uri, values); 1312 } catch (RemoteException e) { 1313 Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e); 1314 return false; 1315 } 1316 1317 return true; 1318 } 1319 1320 private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1321 BufferedReader reader = null; 1322 try { 1323 File f = new File(path); 1324 if (f.exists()) { 1325 reader = new BufferedReader( 1326 new InputStreamReader(new FileInputStream(f)), 8192); 1327 String line = reader.readLine(); 1328 int index = 0; 1329 while (line != null) { 1330 // ignore comment lines, which begin with '#' 1331 if (line.length() > 0 && line.charAt(0) != '#') { 1332 values.clear(); 1333 if (addPlayListEntry(line, playListDirectory, uri, values, index)) 1334 index++; 1335 } 1336 line = reader.readLine(); 1337 } 1338 } 1339 } catch (IOException e) { 1340 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1341 } finally { 1342 try { 1343 if (reader != null) 1344 reader.close(); 1345 } catch (IOException e) { 1346 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1347 } 1348 } 1349 } 1350 1351 private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1352 BufferedReader reader = null; 1353 try { 1354 File f = new File(path); 1355 if (f.exists()) { 1356 reader = new BufferedReader( 1357 new InputStreamReader(new FileInputStream(f)), 8192); 1358 String line = reader.readLine(); 1359 int index = 0; 1360 while (line != null) { 1361 // ignore comment lines, which begin with '#' 1362 if (line.startsWith("File")) { 1363 int equals = line.indexOf('='); 1364 if (equals > 0) { 1365 values.clear(); 1366 if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index)) 1367 index++; 1368 } 1369 } 1370 line = reader.readLine(); 1371 } 1372 } 1373 } catch (IOException e) { 1374 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1375 } finally { 1376 try { 1377 if (reader != null) 1378 reader.close(); 1379 } catch (IOException e) { 1380 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1381 } 1382 } 1383 } 1384 1385 class WplHandler implements ElementListener { 1386 1387 final ContentHandler handler; 1388 String playListDirectory; 1389 Uri uri; 1390 ContentValues values = new ContentValues(); 1391 int index = 0; 1392 1393 public WplHandler(String playListDirectory, Uri uri) { 1394 this.playListDirectory = playListDirectory; 1395 this.uri = uri; 1396 1397 RootElement root = new RootElement("smil"); 1398 Element body = root.getChild("body"); 1399 Element seq = body.getChild("seq"); 1400 Element media = seq.getChild("media"); 1401 media.setElementListener(this); 1402 1403 this.handler = root.getContentHandler(); 1404 } 1405 1406 public void start(Attributes attributes) { 1407 String path = attributes.getValue("", "src"); 1408 if (path != null) { 1409 values.clear(); 1410 if (addPlayListEntry(path, playListDirectory, uri, values, index)) { 1411 index++; 1412 } 1413 } 1414 } 1415 1416 public void end() { 1417 } 1418 1419 ContentHandler getContentHandler() { 1420 return handler; 1421 } 1422 } 1423 1424 private void processWplPlayList(String path, String playListDirectory, Uri uri) { 1425 FileInputStream fis = null; 1426 try { 1427 File f = new File(path); 1428 if (f.exists()) { 1429 fis = new FileInputStream(f); 1430 1431 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler()); 1432 } 1433 } catch (SAXException e) { 1434 e.printStackTrace(); 1435 } catch (IOException e) { 1436 e.printStackTrace(); 1437 } finally { 1438 try { 1439 if (fis != null) 1440 fis.close(); 1441 } catch (IOException e) { 1442 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1443 } 1444 } 1445 } 1446 1447 private void processPlayLists() throws RemoteException { 1448 Iterator<FileCacheEntry> iterator = mPlayLists.iterator(); 1449 while (iterator.hasNext()) { 1450 FileCacheEntry entry = iterator.next(); 1451 String path = entry.mPath; 1452 1453 // only process playlist files if they are new or have been modified since the last scan 1454 if (entry.mLastModifiedChanged) { 1455 ContentValues values = new ContentValues(); 1456 int lastSlash = path.lastIndexOf('/'); 1457 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1458 Uri uri, membersUri; 1459 long rowId = entry.mRowId; 1460 if (rowId == 0) { 1461 // Create a new playlist 1462 1463 int lastDot = path.lastIndexOf('.'); 1464 String name = (lastDot < 0 ? path.substring(lastSlash + 1) : path.substring(lastSlash + 1, lastDot)); 1465 values.put(MediaStore.Audio.Playlists.NAME, name); 1466 values.put(MediaStore.Audio.Playlists.DATA, path); 1467 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1468 uri = mMediaProvider.insert(mPlaylistsUri, values); 1469 rowId = ContentUris.parseId(uri); 1470 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1471 } else { 1472 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1473 1474 // update lastModified value of existing playlist 1475 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1476 mMediaProvider.update(uri, values, null, null); 1477 1478 // delete members of existing playlist 1479 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1480 mMediaProvider.delete(membersUri, null, null); 1481 } 1482 1483 String playListDirectory = path.substring(0, lastSlash + 1); 1484 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1485 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1486 1487 if (fileType == MediaFile.FILE_TYPE_M3U) 1488 processM3uPlayList(path, playListDirectory, membersUri, values); 1489 else if (fileType == MediaFile.FILE_TYPE_PLS) 1490 processPlsPlayList(path, playListDirectory, membersUri, values); 1491 else if (fileType == MediaFile.FILE_TYPE_WPL) 1492 processWplPlayList(path, playListDirectory, membersUri); 1493 1494 Cursor cursor = mMediaProvider.query(membersUri, PLAYLIST_MEMBERS_PROJECTION, null, 1495 null, null); 1496 try { 1497 if (cursor == null || cursor.getCount() == 0) { 1498 Log.d(TAG, "playlist is empty - deleting"); 1499 mMediaProvider.delete(uri, null, null); 1500 } 1501 } finally { 1502 if (cursor != null) cursor.close(); 1503 } 1504 } 1505 } 1506 } 1507 1508 private native void processDirectory(String path, String extensions, MediaScannerClient client); 1509 private native void processFile(String path, String mimeType, MediaScannerClient client); 1510 public native void setLocale(String locale); 1511 1512 public native byte[] extractAlbumArt(FileDescriptor fd); 1513 1514 private static native final void native_init(); 1515 private native final void native_setup(); 1516 private native final void native_finalize(); 1517 @Override 1518 protected void finalize() { 1519 mContext.getContentResolver().releaseProvider(mMediaProvider); 1520 native_finalize(); 1521 } 1522} 1523