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