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