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