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