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