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