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