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