MediaScanner.java revision b2d69e3d7742840fe3b2fb86ded8bec510e7a9ca
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 map.put(Video.Media.DATE_TAKEN, mLastModified * 1000); 634 // FIXME - add RESOLUTION 635 } else if (MediaFile.isImageFileType(mFileType)) { 636 // FIXME - add DESCRIPTION 637 // DATE_TAKEN will be overridden later if this is a JPEG image whose EXIF data 638 // contains date time information. 639 map.put(Images.Media.DATE_TAKEN, mLastModified * 1000); 640 } else if (MediaFile.isAudioFileType(mFileType)) { 641 map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaStore.UNKNOWN_STRING)); 642 map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? 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 if (isAudio) { 718 values.put(Audio.Media.IS_RINGTONE, ringtones); 719 values.put(Audio.Media.IS_NOTIFICATION, notifications); 720 values.put(Audio.Media.IS_ALARM, alarms); 721 values.put(Audio.Media.IS_MUSIC, music); 722 values.put(Audio.Media.IS_PODCAST, podcasts); 723 } else if (mFileType == MediaFile.FILE_TYPE_JPEG) { 724 ExifInterface exif = null; 725 try { 726 exif = new ExifInterface(entry.mPath); 727 } catch (IOException ex) { 728 // exif is null 729 } 730 if (exif != null) { 731 float[] latlng = new float[2]; 732 if (exif.getLatLong(latlng)) { 733 values.put(Images.Media.LATITUDE, latlng[0]); 734 values.put(Images.Media.LONGITUDE, latlng[1]); 735 } 736 737 long time = exif.getGpsDateTime(); 738 if (time != -1) { 739 values.put(Images.Media.DATE_TAKEN, time); 740 } 741 742 int orientation = exif.getAttributeInt( 743 ExifInterface.TAG_ORIENTATION, -1); 744 if (orientation != -1) { 745 // We only recognize a subset of orientation tag values. 746 int degree; 747 switch(orientation) { 748 case ExifInterface.ORIENTATION_ROTATE_90: 749 degree = 90; 750 break; 751 case ExifInterface.ORIENTATION_ROTATE_180: 752 degree = 180; 753 break; 754 case ExifInterface.ORIENTATION_ROTATE_270: 755 degree = 270; 756 break; 757 default: 758 degree = 0; 759 break; 760 } 761 values.put(Images.Media.ORIENTATION, degree); 762 } 763 } 764 } 765 766 Uri result = null; 767 long rowId = entry.mRowId; 768 if (rowId == 0) { 769 // new file, insert it 770 result = mMediaProvider.insert(tableUri, values); 771 if (result != null) { 772 rowId = ContentUris.parseId(result); 773 entry.mRowId = rowId; 774 } 775 } else { 776 // updated file 777 result = ContentUris.withAppendedId(tableUri, rowId); 778 mMediaProvider.update(result, values, null, null); 779 } 780 if (mProcessGenres && mGenre != null) { 781 String genre = mGenre; 782 Uri uri = mGenreCache.get(genre); 783 if (uri == null) { 784 Cursor cursor = null; 785 try { 786 // see if the genre already exists 787 cursor = mMediaProvider.query( 788 mGenresUri, 789 GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?", 790 new String[] { genre }, null); 791 if (cursor == null || cursor.getCount() == 0) { 792 // genre does not exist, so create the genre in the genre table 793 values.clear(); 794 values.put(MediaStore.Audio.Genres.NAME, genre); 795 uri = mMediaProvider.insert(mGenresUri, values); 796 } else { 797 // genre already exists, so compute its Uri 798 cursor.moveToNext(); 799 uri = ContentUris.withAppendedId(mGenresUri, cursor.getLong(0)); 800 } 801 if (uri != null) { 802 uri = Uri.withAppendedPath(uri, Genres.Members.CONTENT_DIRECTORY); 803 mGenreCache.put(genre, uri); 804 } 805 } finally { 806 // release the cursor if it exists 807 if (cursor != null) { 808 cursor.close(); 809 } 810 } 811 } 812 813 if (uri != null) { 814 // add entry to audio_genre_map 815 values.clear(); 816 values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId)); 817 mMediaProvider.insert(uri, values); 818 } 819 } 820 821 if (notifications && !mDefaultNotificationSet) { 822 if (TextUtils.isEmpty(mDefaultNotificationFilename) || 823 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) { 824 setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId); 825 mDefaultNotificationSet = true; 826 } 827 } else if (ringtones && !mDefaultRingtoneSet) { 828 if (TextUtils.isEmpty(mDefaultRingtoneFilename) || 829 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) { 830 setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId); 831 mDefaultRingtoneSet = true; 832 } 833 } else if (alarms && !mDefaultAlarmSet) { 834 if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) || 835 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) { 836 setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId); 837 mDefaultAlarmSet = true; 838 } 839 } 840 841 return result; 842 } 843 844 private boolean doesPathHaveFilename(String path, String filename) { 845 int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1; 846 int filenameLength = filename.length(); 847 return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) && 848 pathFilenameStart + filenameLength == path.length(); 849 } 850 851 private void setSettingIfNotSet(String settingName, Uri uri, long rowId) { 852 853 String existingSettingValue = Settings.System.getString(mContext.getContentResolver(), 854 settingName); 855 856 if (TextUtils.isEmpty(existingSettingValue)) { 857 // Set the setting to the given URI 858 Settings.System.putString(mContext.getContentResolver(), settingName, 859 ContentUris.withAppendedId(uri, rowId).toString()); 860 } 861 } 862 863 public void addNoMediaFolder(String path) { 864 ContentValues values = new ContentValues(); 865 values.put(MediaStore.Images.ImageColumns.DATA, ""); 866 String [] pathSpec = new String[] {path + '%'}; 867 try { 868 mMediaProvider.update(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values, 869 MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec); 870 mMediaProvider.update(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values, 871 MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec); 872 mMediaProvider.update(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values, 873 MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec); 874 } catch (RemoteException e) { 875 throw new RuntimeException(); 876 } 877 } 878 879 }; // end of anonymous MediaScannerClient instance 880 881 private void prescan(String filePath) throws RemoteException { 882 Cursor c = null; 883 String where = null; 884 String[] selectionArgs = null; 885 886 if (mFileCache == null) { 887 mFileCache = new HashMap<String, FileCacheEntry>(); 888 } else { 889 mFileCache.clear(); 890 } 891 if (mPlayLists == null) { 892 mPlayLists = new ArrayList<FileCacheEntry>(); 893 } else { 894 mPlayLists.clear(); 895 } 896 897 // Build the list of files from the content provider 898 try { 899 // Read existing files from the audio table 900 if (filePath != null) { 901 where = MediaStore.Audio.Media.DATA + "=?"; 902 selectionArgs = new String[] { filePath }; 903 } 904 c = mMediaProvider.query(mAudioUri, AUDIO_PROJECTION, where, selectionArgs, null); 905 906 if (c != null) { 907 try { 908 while (c.moveToNext()) { 909 long rowId = c.getLong(ID_AUDIO_COLUMN_INDEX); 910 String path = c.getString(PATH_AUDIO_COLUMN_INDEX); 911 long lastModified = c.getLong(DATE_MODIFIED_AUDIO_COLUMN_INDEX); 912 913 // Only consider entries with absolute path names. 914 // This allows storing URIs in the database without the 915 // media scanner removing them. 916 if (path.startsWith("/")) { 917 String key = path; 918 if (mCaseInsensitivePaths) { 919 key = path.toLowerCase(); 920 } 921 mFileCache.put(key, new FileCacheEntry(mAudioUri, rowId, path, 922 lastModified)); 923 } 924 } 925 } finally { 926 c.close(); 927 c = null; 928 } 929 } 930 931 // Read existing files from the video table 932 if (filePath != null) { 933 where = MediaStore.Video.Media.DATA + "=?"; 934 } else { 935 where = null; 936 } 937 c = mMediaProvider.query(mVideoUri, VIDEO_PROJECTION, where, selectionArgs, null); 938 939 if (c != null) { 940 try { 941 while (c.moveToNext()) { 942 long rowId = c.getLong(ID_VIDEO_COLUMN_INDEX); 943 String path = c.getString(PATH_VIDEO_COLUMN_INDEX); 944 long lastModified = c.getLong(DATE_MODIFIED_VIDEO_COLUMN_INDEX); 945 946 // Only consider entries with absolute path names. 947 // This allows storing URIs in the database without the 948 // media scanner removing them. 949 if (path.startsWith("/")) { 950 String key = path; 951 if (mCaseInsensitivePaths) { 952 key = path.toLowerCase(); 953 } 954 mFileCache.put(key, new FileCacheEntry(mVideoUri, rowId, path, 955 lastModified)); 956 } 957 } 958 } finally { 959 c.close(); 960 c = null; 961 } 962 } 963 964 // Read existing files from the images table 965 if (filePath != null) { 966 where = MediaStore.Images.Media.DATA + "=?"; 967 } else { 968 where = null; 969 } 970 mOriginalCount = 0; 971 c = mMediaProvider.query(mImagesUri, IMAGES_PROJECTION, where, selectionArgs, null); 972 973 if (c != null) { 974 try { 975 mOriginalCount = c.getCount(); 976 while (c.moveToNext()) { 977 long rowId = c.getLong(ID_IMAGES_COLUMN_INDEX); 978 String path = c.getString(PATH_IMAGES_COLUMN_INDEX); 979 long lastModified = c.getLong(DATE_MODIFIED_IMAGES_COLUMN_INDEX); 980 981 // Only consider entries with absolute path names. 982 // This allows storing URIs in the database without the 983 // media scanner removing them. 984 if (path.startsWith("/")) { 985 String key = path; 986 if (mCaseInsensitivePaths) { 987 key = path.toLowerCase(); 988 } 989 mFileCache.put(key, new FileCacheEntry(mImagesUri, rowId, path, 990 lastModified)); 991 } 992 } 993 } finally { 994 c.close(); 995 c = null; 996 } 997 } 998 999 if (mProcessPlaylists) { 1000 // Read existing files from the playlists table 1001 if (filePath != null) { 1002 where = MediaStore.Audio.Playlists.DATA + "=?"; 1003 } else { 1004 where = null; 1005 } 1006 c = mMediaProvider.query(mPlaylistsUri, PLAYLISTS_PROJECTION, where, selectionArgs, null); 1007 1008 if (c != null) { 1009 try { 1010 while (c.moveToNext()) { 1011 String path = c.getString(PATH_PLAYLISTS_COLUMN_INDEX); 1012 1013 if (path != null && path.length() > 0) { 1014 long rowId = c.getLong(ID_PLAYLISTS_COLUMN_INDEX); 1015 long lastModified = c.getLong(DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX); 1016 1017 String key = path; 1018 if (mCaseInsensitivePaths) { 1019 key = path.toLowerCase(); 1020 } 1021 mFileCache.put(key, new FileCacheEntry(mPlaylistsUri, rowId, path, 1022 lastModified)); 1023 } 1024 } 1025 } finally { 1026 c.close(); 1027 c = null; 1028 } 1029 } 1030 } 1031 } 1032 finally { 1033 if (c != null) { 1034 c.close(); 1035 } 1036 } 1037 } 1038 1039 private boolean inScanDirectory(String path, String[] directories) { 1040 for (int i = 0; i < directories.length; i++) { 1041 if (path.startsWith(directories[i])) { 1042 return true; 1043 } 1044 } 1045 return false; 1046 } 1047 1048 private void pruneDeadThumbnailFiles() { 1049 HashSet<String> existingFiles = new HashSet<String>(); 1050 String directory = "/sdcard/DCIM/.thumbnails"; 1051 String [] files = (new File(directory)).list(); 1052 if (files == null) 1053 files = new String[0]; 1054 1055 for (int i = 0; i < files.length; i++) { 1056 String fullPathString = directory + "/" + files[i]; 1057 existingFiles.add(fullPathString); 1058 } 1059 1060 try { 1061 Cursor c = mMediaProvider.query( 1062 mThumbsUri, 1063 new String [] { "_data" }, 1064 null, 1065 null, 1066 null); 1067 Log.v(TAG, "pruneDeadThumbnailFiles... " + c); 1068 if (c != null && c.moveToFirst()) { 1069 do { 1070 String fullPathString = c.getString(0); 1071 existingFiles.remove(fullPathString); 1072 } while (c.moveToNext()); 1073 } 1074 1075 for (String fileToDelete : existingFiles) { 1076 if (Config.LOGV) 1077 Log.v(TAG, "fileToDelete is " + fileToDelete); 1078 try { 1079 (new File(fileToDelete)).delete(); 1080 } catch (SecurityException ex) { 1081 } 1082 } 1083 1084 Log.v(TAG, "/pruneDeadThumbnailFiles... " + c); 1085 if (c != null) { 1086 c.close(); 1087 } 1088 } catch (RemoteException e) { 1089 // We will soon be killed... 1090 } 1091 } 1092 1093 private void postscan(String[] directories) throws RemoteException { 1094 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 1095 1096 while (iterator.hasNext()) { 1097 FileCacheEntry entry = iterator.next(); 1098 String path = entry.mPath; 1099 1100 // remove database entries for files that no longer exist. 1101 boolean fileMissing = false; 1102 1103 if (!entry.mSeenInFileSystem) { 1104 if (inScanDirectory(path, directories)) { 1105 // we didn't see this file in the scan directory. 1106 fileMissing = true; 1107 } else { 1108 // the file is outside of our scan directory, 1109 // so we need to check for file existence here. 1110 File testFile = new File(path); 1111 if (!testFile.exists()) { 1112 fileMissing = true; 1113 } 1114 } 1115 } 1116 1117 if (fileMissing) { 1118 // do not delete missing playlists, since they may have been modified by the user. 1119 // the user can delete them in the media player instead. 1120 // instead, clear the path and lastModified fields in the row 1121 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1122 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1123 1124 if (MediaFile.isPlayListFileType(fileType)) { 1125 ContentValues values = new ContentValues(); 1126 values.put(MediaStore.Audio.Playlists.DATA, ""); 1127 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, 0); 1128 mMediaProvider.update(ContentUris.withAppendedId(mPlaylistsUri, entry.mRowId), values, null, null); 1129 } else { 1130 mMediaProvider.delete(ContentUris.withAppendedId(entry.mTableUri, entry.mRowId), null, null); 1131 iterator.remove(); 1132 } 1133 } 1134 } 1135 1136 // handle playlists last, after we know what media files are on the storage. 1137 if (mProcessPlaylists) { 1138 processPlayLists(); 1139 } 1140 1141 if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external"))) 1142 pruneDeadThumbnailFiles(); 1143 1144 // allow GC to clean up 1145 mGenreCache = null; 1146 mPlayLists = null; 1147 mFileCache = null; 1148 mMediaProvider = null; 1149 } 1150 1151 private void initialize(String volumeName) { 1152 mMediaProvider = mContext.getContentResolver().acquireProvider("media"); 1153 1154 mAudioUri = Audio.Media.getContentUri(volumeName); 1155 mVideoUri = Video.Media.getContentUri(volumeName); 1156 mImagesUri = Images.Media.getContentUri(volumeName); 1157 mThumbsUri = Images.Thumbnails.getContentUri(volumeName); 1158 1159 if (!volumeName.equals("internal")) { 1160 // we only support playlists on external media 1161 mProcessPlaylists = true; 1162 mProcessGenres = true; 1163 mGenreCache = new HashMap<String, Uri>(); 1164 mGenresUri = Genres.getContentUri(volumeName); 1165 mPlaylistsUri = Playlists.getContentUri(volumeName); 1166 // assuming external storage is FAT (case insensitive), except on the simulator. 1167 if ( Process.supportsProcesses()) { 1168 mCaseInsensitivePaths = true; 1169 } 1170 } 1171 } 1172 1173 public void scanDirectories(String[] directories, String volumeName) { 1174 try { 1175 long start = System.currentTimeMillis(); 1176 initialize(volumeName); 1177 prescan(null); 1178 long prescan = System.currentTimeMillis(); 1179 1180 for (int i = 0; i < directories.length; i++) { 1181 processDirectory(directories[i], MediaFile.sFileExtensions, mClient); 1182 } 1183 long scan = System.currentTimeMillis(); 1184 postscan(directories); 1185 long end = System.currentTimeMillis(); 1186 1187 if (Config.LOGD) { 1188 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1189 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1190 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1191 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1192 } 1193 } catch (SQLException e) { 1194 // this might happen if the SD card is removed while the media scanner is running 1195 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1196 } catch (UnsupportedOperationException e) { 1197 // this might happen if the SD card is removed while the media scanner is running 1198 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1199 } catch (RemoteException e) { 1200 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1201 } 1202 } 1203 1204 // this function is used to scan a single file 1205 public Uri scanSingleFile(String path, String volumeName, String mimeType) { 1206 try { 1207 initialize(volumeName); 1208 prescan(path); 1209 1210 File file = new File(path); 1211 // always scan the file, so we can return the content://media Uri for existing files 1212 return mClient.doScanFile(path, mimeType, file.lastModified(), file.length(), true); 1213 } catch (RemoteException e) { 1214 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1215 return null; 1216 } 1217 } 1218 1219 // returns the number of matching file/directory names, starting from the right 1220 private int matchPaths(String path1, String path2) { 1221 int result = 0; 1222 int end1 = path1.length(); 1223 int end2 = path2.length(); 1224 1225 while (end1 > 0 && end2 > 0) { 1226 int slash1 = path1.lastIndexOf('/', end1 - 1); 1227 int slash2 = path2.lastIndexOf('/', end2 - 1); 1228 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1229 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1230 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1231 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1232 if (start1 < 0) start1 = 0; else start1++; 1233 if (start2 < 0) start2 = 0; else start2++; 1234 int length = end1 - start1; 1235 if (end2 - start2 != length) break; 1236 if (path1.regionMatches(true, start1, path2, start2, length)) { 1237 result++; 1238 end1 = start1 - 1; 1239 end2 = start2 - 1; 1240 } else break; 1241 } 1242 1243 return result; 1244 } 1245 1246 private boolean addPlayListEntry(String entry, String playListDirectory, 1247 Uri uri, ContentValues values, int index) { 1248 1249 // watch for trailing whitespace 1250 int entryLength = entry.length(); 1251 while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--; 1252 // path should be longer than 3 characters. 1253 // avoid index out of bounds errors below by returning here. 1254 if (entryLength < 3) return false; 1255 if (entryLength < entry.length()) entry = entry.substring(0, entryLength); 1256 1257 // does entry appear to be an absolute path? 1258 // look for Unix or DOS absolute paths 1259 char ch1 = entry.charAt(0); 1260 boolean fullPath = (ch1 == '/' || 1261 (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\')); 1262 // if we have a relative path, combine entry with playListDirectory 1263 if (!fullPath) 1264 entry = playListDirectory + entry; 1265 1266 //FIXME - should we look for "../" within the path? 1267 1268 // best matching MediaFile for the play list entry 1269 FileCacheEntry bestMatch = null; 1270 1271 // number of rightmost file/directory names for bestMatch 1272 int bestMatchLength = 0; 1273 1274 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 1275 while (iterator.hasNext()) { 1276 FileCacheEntry cacheEntry = iterator.next(); 1277 String path = cacheEntry.mPath; 1278 1279 if (path.equalsIgnoreCase(entry)) { 1280 bestMatch = cacheEntry; 1281 break; // don't bother continuing search 1282 } 1283 1284 int matchLength = matchPaths(path, entry); 1285 if (matchLength > bestMatchLength) { 1286 bestMatch = cacheEntry; 1287 bestMatchLength = matchLength; 1288 } 1289 } 1290 1291 // if the match is not for an audio file, bail out 1292 if (bestMatch == null || ! mAudioUri.equals(bestMatch.mTableUri)) { 1293 return false; 1294 } 1295 1296 try { 1297 // OK, now we need to add this to the database 1298 values.clear(); 1299 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1300 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId)); 1301 mMediaProvider.insert(uri, values); 1302 } catch (RemoteException e) { 1303 Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e); 1304 return false; 1305 } 1306 1307 return true; 1308 } 1309 1310 private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1311 BufferedReader reader = null; 1312 try { 1313 File f = new File(path); 1314 if (f.exists()) { 1315 reader = new BufferedReader( 1316 new InputStreamReader(new FileInputStream(f)), 8192); 1317 String line = reader.readLine(); 1318 int index = 0; 1319 while (line != null) { 1320 // ignore comment lines, which begin with '#' 1321 if (line.length() > 0 && line.charAt(0) != '#') { 1322 values.clear(); 1323 if (addPlayListEntry(line, playListDirectory, uri, values, index)) 1324 index++; 1325 } 1326 line = reader.readLine(); 1327 } 1328 } 1329 } catch (IOException e) { 1330 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1331 } finally { 1332 try { 1333 if (reader != null) 1334 reader.close(); 1335 } catch (IOException e) { 1336 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1337 } 1338 } 1339 } 1340 1341 private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1342 BufferedReader reader = null; 1343 try { 1344 File f = new File(path); 1345 if (f.exists()) { 1346 reader = new BufferedReader( 1347 new InputStreamReader(new FileInputStream(f)), 8192); 1348 String line = reader.readLine(); 1349 int index = 0; 1350 while (line != null) { 1351 // ignore comment lines, which begin with '#' 1352 if (line.startsWith("File")) { 1353 int equals = line.indexOf('='); 1354 if (equals > 0) { 1355 values.clear(); 1356 if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index)) 1357 index++; 1358 } 1359 } 1360 line = reader.readLine(); 1361 } 1362 } 1363 } catch (IOException e) { 1364 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1365 } finally { 1366 try { 1367 if (reader != null) 1368 reader.close(); 1369 } catch (IOException e) { 1370 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1371 } 1372 } 1373 } 1374 1375 class WplHandler implements ElementListener { 1376 1377 final ContentHandler handler; 1378 String playListDirectory; 1379 Uri uri; 1380 ContentValues values = new ContentValues(); 1381 int index = 0; 1382 1383 public WplHandler(String playListDirectory, Uri uri) { 1384 this.playListDirectory = playListDirectory; 1385 this.uri = uri; 1386 1387 RootElement root = new RootElement("smil"); 1388 Element body = root.getChild("body"); 1389 Element seq = body.getChild("seq"); 1390 Element media = seq.getChild("media"); 1391 media.setElementListener(this); 1392 1393 this.handler = root.getContentHandler(); 1394 } 1395 1396 public void start(Attributes attributes) { 1397 String path = attributes.getValue("", "src"); 1398 if (path != null) { 1399 values.clear(); 1400 if (addPlayListEntry(path, playListDirectory, uri, values, index)) { 1401 index++; 1402 } 1403 } 1404 } 1405 1406 public void end() { 1407 } 1408 1409 ContentHandler getContentHandler() { 1410 return handler; 1411 } 1412 } 1413 1414 private void processWplPlayList(String path, String playListDirectory, Uri uri) { 1415 FileInputStream fis = null; 1416 try { 1417 File f = new File(path); 1418 if (f.exists()) { 1419 fis = new FileInputStream(f); 1420 1421 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler()); 1422 } 1423 } catch (SAXException e) { 1424 e.printStackTrace(); 1425 } catch (IOException e) { 1426 e.printStackTrace(); 1427 } finally { 1428 try { 1429 if (fis != null) 1430 fis.close(); 1431 } catch (IOException e) { 1432 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1433 } 1434 } 1435 } 1436 1437 private void processPlayLists() throws RemoteException { 1438 Iterator<FileCacheEntry> iterator = mPlayLists.iterator(); 1439 while (iterator.hasNext()) { 1440 FileCacheEntry entry = iterator.next(); 1441 String path = entry.mPath; 1442 1443 // only process playlist files if they are new or have been modified since the last scan 1444 if (entry.mLastModifiedChanged) { 1445 ContentValues values = new ContentValues(); 1446 int lastSlash = path.lastIndexOf('/'); 1447 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1448 Uri uri, membersUri; 1449 long rowId = entry.mRowId; 1450 if (rowId == 0) { 1451 // Create a new playlist 1452 1453 int lastDot = path.lastIndexOf('.'); 1454 String name = (lastDot < 0 ? path.substring(lastSlash + 1) : path.substring(lastSlash + 1, lastDot)); 1455 values.put(MediaStore.Audio.Playlists.NAME, name); 1456 values.put(MediaStore.Audio.Playlists.DATA, path); 1457 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1458 uri = mMediaProvider.insert(mPlaylistsUri, values); 1459 rowId = ContentUris.parseId(uri); 1460 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1461 } else { 1462 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1463 1464 // update lastModified value of existing playlist 1465 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1466 mMediaProvider.update(uri, values, null, null); 1467 1468 // delete members of existing playlist 1469 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1470 mMediaProvider.delete(membersUri, null, null); 1471 } 1472 1473 String playListDirectory = path.substring(0, lastSlash + 1); 1474 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1475 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1476 1477 if (fileType == MediaFile.FILE_TYPE_M3U) 1478 processM3uPlayList(path, playListDirectory, membersUri, values); 1479 else if (fileType == MediaFile.FILE_TYPE_PLS) 1480 processPlsPlayList(path, playListDirectory, membersUri, values); 1481 else if (fileType == MediaFile.FILE_TYPE_WPL) 1482 processWplPlayList(path, playListDirectory, membersUri); 1483 1484 Cursor cursor = mMediaProvider.query(membersUri, PLAYLIST_MEMBERS_PROJECTION, null, 1485 null, null); 1486 try { 1487 if (cursor == null || cursor.getCount() == 0) { 1488 Log.d(TAG, "playlist is empty - deleting"); 1489 mMediaProvider.delete(uri, null, null); 1490 } 1491 } finally { 1492 if (cursor != null) cursor.close(); 1493 } 1494 } 1495 } 1496 } 1497 1498 private native void processDirectory(String path, String extensions, MediaScannerClient client); 1499 private native void processFile(String path, String mimeType, MediaScannerClient client); 1500 public native void setLocale(String locale); 1501 1502 public native byte[] extractAlbumArt(FileDescriptor fd); 1503 1504 private static native final void native_init(); 1505 private native final void native_setup(); 1506 private native final void native_finalize(); 1507 @Override 1508 protected void finalize() { 1509 mContext.getContentResolver().releaseProvider(mMediaProvider); 1510 native_finalize(); 1511 } 1512} 1513