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