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