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