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