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