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 17 package android.media; 18 19 import org.xml.sax.Attributes; 20 import org.xml.sax.ContentHandler; 21 import org.xml.sax.SAXException; 22 23 import android.content.ContentUris; 24 import android.content.ContentValues; 25 import android.content.Context; 26 import android.content.IContentProvider; 27 import android.database.Cursor; 28 import android.database.SQLException; 29 import android.graphics.BitmapFactory; 30 import android.net.Uri; 31 import android.os.Process; 32 import android.os.RemoteException; 33 import android.os.SystemProperties; 34 import android.provider.MediaStore; 35 import android.provider.Settings; 36 import android.provider.MediaStore.Audio; 37 import android.provider.MediaStore.Images; 38 import android.provider.MediaStore.Video; 39 import android.provider.MediaStore.Audio.Genres; 40 import android.provider.MediaStore.Audio.Playlists; 41 import android.sax.Element; 42 import android.sax.ElementListener; 43 import android.sax.RootElement; 44 import android.text.TextUtils; 45 import android.util.Config; 46 import android.util.Log; 47 import android.util.Xml; 48 49 import java.io.BufferedReader; 50 import java.io.File; 51 import java.io.FileDescriptor; 52 import java.io.FileInputStream; 53 import java.io.IOException; 54 import java.io.InputStreamReader; 55 import java.util.ArrayList; 56 import java.util.HashMap; 57 import java.util.HashSet; 58 import 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 */ 103 public class MediaScanner 104 { 105 static { 106 System.loadLibrary("media_jni"); native_init()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 FileCacheEntry(Uri tableUri, long rowId, String path, long lastModified)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 toString()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 MediaScanner(Context c)368 public MediaScanner(Context c) { 369 native_setup(); 370 mContext = c; 371 mBitmapOptions.inSampleSize = 1; 372 mBitmapOptions.inJustDecodeBounds = true; 373 374 setDefaultRingtoneFileNames(); 375 } 376 setDefaultRingtoneFileNames()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 private int mCompilation; 406 beginFile(String path, String mimeType, long lastModified, long fileSize)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 mCompilation = 0; 491 492 return entry; 493 } 494 scanFile(String path, long lastModified, long fileSize)495 public void scanFile(String path, long lastModified, long fileSize) { 496 // This is the callback funtion from native codes. 497 // Log.v(TAG, "scanFile: "+path); 498 doScanFile(path, null, lastModified, fileSize, false); 499 } 500 scanFile(String path, String mimeType, long lastModified, long fileSize)501 public void scanFile(String path, String mimeType, long lastModified, long fileSize) { 502 doScanFile(path, mimeType, lastModified, fileSize, false); 503 } 504 doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean scanAlways)505 public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean scanAlways) { 506 Uri result = null; 507 // long t1 = System.currentTimeMillis(); 508 try { 509 FileCacheEntry entry = beginFile(path, mimeType, lastModified, fileSize); 510 // rescan for metadata if file was modified since last scan 511 if (entry != null && (entry.mLastModifiedChanged || scanAlways)) { 512 String lowpath = path.toLowerCase(); 513 boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0); 514 boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0); 515 boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0); 516 boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0); 517 boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) || 518 (!ringtones && !notifications && !alarms && !podcasts); 519 520 if (!MediaFile.isImageFileType(mFileType)) { 521 processFile(path, mimeType, this); 522 } 523 524 result = endFile(entry, ringtones, notifications, alarms, music, podcasts); 525 } 526 } catch (RemoteException e) { 527 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 528 } 529 // long t2 = System.currentTimeMillis(); 530 // Log.v(TAG, "scanFile: " + path + " took " + (t2-t1)); 531 return result; 532 } 533 parseSubstring(String s, int start, int defaultValue)534 private int parseSubstring(String s, int start, int defaultValue) { 535 int length = s.length(); 536 if (start == length) return defaultValue; 537 538 char ch = s.charAt(start++); 539 // return defaultValue if we have no integer at all 540 if (ch < '0' || ch > '9') return defaultValue; 541 542 int result = ch - '0'; 543 while (start < length) { 544 ch = s.charAt(start++); 545 if (ch < '0' || ch > '9') return result; 546 result = result * 10 + (ch - '0'); 547 } 548 549 return result; 550 } 551 handleStringTag(String name, String value)552 public void handleStringTag(String name, String value) { 553 if (name.equalsIgnoreCase("title") || name.startsWith("title;")) { 554 // Don't trim() here, to preserve the special \001 character 555 // used to force sorting. The media provider will trim() before 556 // inserting the title in to the database. 557 mTitle = value; 558 } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) { 559 mArtist = value.trim(); 560 } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")) { 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 } else if (name.equalsIgnoreCase("compilation")) { 602 mCompilation = parseSubstring(value, 0, 0); 603 } 604 } 605 setMimeType(String mimeType)606 public void setMimeType(String mimeType) { 607 if ("audio/mp4".equals(mMimeType) && 608 mimeType.startsWith("video")) { 609 // for feature parity with Donut, we force m4a files to keep the 610 // audio/mp4 mimetype, even if they are really "enhanced podcasts" 611 // with a video track 612 return; 613 } 614 mMimeType = mimeType; 615 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 616 } 617 618 /** 619 * Formats the data into a values array suitable for use with the Media 620 * Content Provider. 621 * 622 * @return a map of values 623 */ toValues()624 private ContentValues toValues() { 625 ContentValues map = new ContentValues(); 626 627 map.put(MediaStore.MediaColumns.DATA, mPath); 628 map.put(MediaStore.MediaColumns.TITLE, mTitle); 629 map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified); 630 map.put(MediaStore.MediaColumns.SIZE, mFileSize); 631 map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType); 632 633 if (MediaFile.isVideoFileType(mFileType)) { 634 map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaStore.UNKNOWN_STRING)); 635 map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaStore.UNKNOWN_STRING)); 636 map.put(Video.Media.DURATION, mDuration); 637 // FIXME - add RESOLUTION 638 } else if (MediaFile.isImageFileType(mFileType)) { 639 // FIXME - add DESCRIPTION 640 } else if (MediaFile.isAudioFileType(mFileType)) { 641 map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ? 642 mArtist : MediaStore.UNKNOWN_STRING); 643 map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null && 644 mAlbumArtist.length() > 0) ? mAlbumArtist : null); 645 map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ? 646 mAlbum : MediaStore.UNKNOWN_STRING); 647 map.put(Audio.Media.COMPOSER, mComposer); 648 if (mYear != 0) { 649 map.put(Audio.Media.YEAR, mYear); 650 } 651 map.put(Audio.Media.TRACK, mTrack); 652 map.put(Audio.Media.DURATION, mDuration); 653 map.put(Audio.Media.COMPILATION, mCompilation); 654 } 655 return map; 656 } 657 endFile(FileCacheEntry entry, boolean ringtones, boolean notifications, boolean alarms, boolean music, boolean podcasts)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 doesPathHaveFilename(String path, String filename)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 setSettingIfNotSet(String settingName, Uri uri, long rowId)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 addNoMediaFolder(String path)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 prescan(String filePath)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 inScanDirectory(String path, String[] directories)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 pruneDeadThumbnailFiles()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 postscan(String[] directories)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 initialize(String volumeName)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 // assuming external storage is FAT (case insensitive), except on the simulator. 1179 if ( Process.supportsProcesses()) { 1180 mCaseInsensitivePaths = true; 1181 } 1182 } 1183 } 1184 scanDirectories(String[] directories, String volumeName)1185 public void scanDirectories(String[] directories, String volumeName) { 1186 try { 1187 long start = System.currentTimeMillis(); 1188 initialize(volumeName); 1189 prescan(null); 1190 long prescan = System.currentTimeMillis(); 1191 1192 for (int i = 0; i < directories.length; i++) { 1193 processDirectory(directories[i], MediaFile.sFileExtensions, mClient); 1194 } 1195 long scan = System.currentTimeMillis(); 1196 postscan(directories); 1197 long end = System.currentTimeMillis(); 1198 1199 if (Config.LOGD) { 1200 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1201 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1202 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1203 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1204 } 1205 } catch (SQLException e) { 1206 // this might happen if the SD card is removed while the media scanner is running 1207 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1208 } catch (UnsupportedOperationException e) { 1209 // this might happen if the SD card is removed while the media scanner is running 1210 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1211 } catch (RemoteException e) { 1212 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1213 } 1214 } 1215 1216 // this function is used to scan a single file scanSingleFile(String path, String volumeName, String mimeType)1217 public Uri scanSingleFile(String path, String volumeName, String mimeType) { 1218 try { 1219 initialize(volumeName); 1220 prescan(path); 1221 1222 File file = new File(path); 1223 // always scan the file, so we can return the content://media Uri for existing files 1224 return mClient.doScanFile(path, mimeType, file.lastModified(), file.length(), true); 1225 } catch (RemoteException e) { 1226 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1227 return null; 1228 } 1229 } 1230 1231 // returns the number of matching file/directory names, starting from the right matchPaths(String path1, String path2)1232 private int matchPaths(String path1, String path2) { 1233 int result = 0; 1234 int end1 = path1.length(); 1235 int end2 = path2.length(); 1236 1237 while (end1 > 0 && end2 > 0) { 1238 int slash1 = path1.lastIndexOf('/', end1 - 1); 1239 int slash2 = path2.lastIndexOf('/', end2 - 1); 1240 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1241 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1242 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1243 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1244 if (start1 < 0) start1 = 0; else start1++; 1245 if (start2 < 0) start2 = 0; else start2++; 1246 int length = end1 - start1; 1247 if (end2 - start2 != length) break; 1248 if (path1.regionMatches(true, start1, path2, start2, length)) { 1249 result++; 1250 end1 = start1 - 1; 1251 end2 = start2 - 1; 1252 } else break; 1253 } 1254 1255 return result; 1256 } 1257 addPlayListEntry(String entry, String playListDirectory, Uri uri, ContentValues values, int index)1258 private boolean addPlayListEntry(String entry, String playListDirectory, 1259 Uri uri, ContentValues values, int index) { 1260 1261 // watch for trailing whitespace 1262 int entryLength = entry.length(); 1263 while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--; 1264 // path should be longer than 3 characters. 1265 // avoid index out of bounds errors below by returning here. 1266 if (entryLength < 3) return false; 1267 if (entryLength < entry.length()) entry = entry.substring(0, entryLength); 1268 1269 // does entry appear to be an absolute path? 1270 // look for Unix or DOS absolute paths 1271 char ch1 = entry.charAt(0); 1272 boolean fullPath = (ch1 == '/' || 1273 (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\')); 1274 // if we have a relative path, combine entry with playListDirectory 1275 if (!fullPath) 1276 entry = playListDirectory + entry; 1277 1278 //FIXME - should we look for "../" within the path? 1279 1280 // best matching MediaFile for the play list entry 1281 FileCacheEntry bestMatch = null; 1282 1283 // number of rightmost file/directory names for bestMatch 1284 int bestMatchLength = 0; 1285 1286 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 1287 while (iterator.hasNext()) { 1288 FileCacheEntry cacheEntry = iterator.next(); 1289 String path = cacheEntry.mPath; 1290 1291 if (path.equalsIgnoreCase(entry)) { 1292 bestMatch = cacheEntry; 1293 break; // don't bother continuing search 1294 } 1295 1296 int matchLength = matchPaths(path, entry); 1297 if (matchLength > bestMatchLength) { 1298 bestMatch = cacheEntry; 1299 bestMatchLength = matchLength; 1300 } 1301 } 1302 1303 // if the match is not for an audio file, bail out 1304 if (bestMatch == null || ! mAudioUri.equals(bestMatch.mTableUri)) { 1305 return false; 1306 } 1307 1308 try { 1309 // OK, now we need to add this to the database 1310 values.clear(); 1311 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1312 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId)); 1313 mMediaProvider.insert(uri, values); 1314 } catch (RemoteException e) { 1315 Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e); 1316 return false; 1317 } 1318 1319 return true; 1320 } 1321 processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values)1322 private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1323 BufferedReader reader = null; 1324 try { 1325 File f = new File(path); 1326 if (f.exists()) { 1327 reader = new BufferedReader( 1328 new InputStreamReader(new FileInputStream(f)), 8192); 1329 String line = reader.readLine(); 1330 int index = 0; 1331 while (line != null) { 1332 // ignore comment lines, which begin with '#' 1333 if (line.length() > 0 && line.charAt(0) != '#') { 1334 values.clear(); 1335 if (addPlayListEntry(line, playListDirectory, uri, values, index)) 1336 index++; 1337 } 1338 line = reader.readLine(); 1339 } 1340 } 1341 } catch (IOException e) { 1342 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1343 } finally { 1344 try { 1345 if (reader != null) 1346 reader.close(); 1347 } catch (IOException e) { 1348 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1349 } 1350 } 1351 } 1352 processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values)1353 private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1354 BufferedReader reader = null; 1355 try { 1356 File f = new File(path); 1357 if (f.exists()) { 1358 reader = new BufferedReader( 1359 new InputStreamReader(new FileInputStream(f)), 8192); 1360 String line = reader.readLine(); 1361 int index = 0; 1362 while (line != null) { 1363 // ignore comment lines, which begin with '#' 1364 if (line.startsWith("File")) { 1365 int equals = line.indexOf('='); 1366 if (equals > 0) { 1367 values.clear(); 1368 if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index)) 1369 index++; 1370 } 1371 } 1372 line = reader.readLine(); 1373 } 1374 } 1375 } catch (IOException e) { 1376 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1377 } finally { 1378 try { 1379 if (reader != null) 1380 reader.close(); 1381 } catch (IOException e) { 1382 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1383 } 1384 } 1385 } 1386 1387 class WplHandler implements ElementListener { 1388 1389 final ContentHandler handler; 1390 String playListDirectory; 1391 Uri uri; 1392 ContentValues values = new ContentValues(); 1393 int index = 0; 1394 WplHandler(String playListDirectory, Uri uri)1395 public WplHandler(String playListDirectory, Uri uri) { 1396 this.playListDirectory = playListDirectory; 1397 this.uri = uri; 1398 1399 RootElement root = new RootElement("smil"); 1400 Element body = root.getChild("body"); 1401 Element seq = body.getChild("seq"); 1402 Element media = seq.getChild("media"); 1403 media.setElementListener(this); 1404 1405 this.handler = root.getContentHandler(); 1406 } 1407 start(Attributes attributes)1408 public void start(Attributes attributes) { 1409 String path = attributes.getValue("", "src"); 1410 if (path != null) { 1411 values.clear(); 1412 if (addPlayListEntry(path, playListDirectory, uri, values, index)) { 1413 index++; 1414 } 1415 } 1416 } 1417 end()1418 public void end() { 1419 } 1420 getContentHandler()1421 ContentHandler getContentHandler() { 1422 return handler; 1423 } 1424 } 1425 processWplPlayList(String path, String playListDirectory, Uri uri)1426 private void processWplPlayList(String path, String playListDirectory, Uri uri) { 1427 FileInputStream fis = null; 1428 try { 1429 File f = new File(path); 1430 if (f.exists()) { 1431 fis = new FileInputStream(f); 1432 1433 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler()); 1434 } 1435 } catch (SAXException e) { 1436 e.printStackTrace(); 1437 } catch (IOException e) { 1438 e.printStackTrace(); 1439 } finally { 1440 try { 1441 if (fis != null) 1442 fis.close(); 1443 } catch (IOException e) { 1444 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1445 } 1446 } 1447 } 1448 processPlayLists()1449 private void processPlayLists() throws RemoteException { 1450 Iterator<FileCacheEntry> iterator = mPlayLists.iterator(); 1451 while (iterator.hasNext()) { 1452 FileCacheEntry entry = iterator.next(); 1453 String path = entry.mPath; 1454 1455 // only process playlist files if they are new or have been modified since the last scan 1456 if (entry.mLastModifiedChanged) { 1457 ContentValues values = new ContentValues(); 1458 int lastSlash = path.lastIndexOf('/'); 1459 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1460 Uri uri, membersUri; 1461 long rowId = entry.mRowId; 1462 if (rowId == 0) { 1463 // Create a new playlist 1464 1465 int lastDot = path.lastIndexOf('.'); 1466 String name = (lastDot < 0 ? path.substring(lastSlash + 1) : path.substring(lastSlash + 1, lastDot)); 1467 values.put(MediaStore.Audio.Playlists.NAME, name); 1468 values.put(MediaStore.Audio.Playlists.DATA, path); 1469 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1470 uri = mMediaProvider.insert(mPlaylistsUri, values); 1471 rowId = ContentUris.parseId(uri); 1472 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1473 } else { 1474 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1475 1476 // update lastModified value of existing playlist 1477 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1478 mMediaProvider.update(uri, values, null, null); 1479 1480 // delete members of existing playlist 1481 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1482 mMediaProvider.delete(membersUri, null, null); 1483 } 1484 1485 String playListDirectory = path.substring(0, lastSlash + 1); 1486 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1487 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1488 1489 if (fileType == MediaFile.FILE_TYPE_M3U) 1490 processM3uPlayList(path, playListDirectory, membersUri, values); 1491 else if (fileType == MediaFile.FILE_TYPE_PLS) 1492 processPlsPlayList(path, playListDirectory, membersUri, values); 1493 else if (fileType == MediaFile.FILE_TYPE_WPL) 1494 processWplPlayList(path, playListDirectory, membersUri); 1495 1496 Cursor cursor = mMediaProvider.query(membersUri, PLAYLIST_MEMBERS_PROJECTION, null, 1497 null, null); 1498 try { 1499 if (cursor == null || cursor.getCount() == 0) { 1500 Log.d(TAG, "playlist is empty - deleting"); 1501 mMediaProvider.delete(uri, null, null); 1502 } 1503 } finally { 1504 if (cursor != null) cursor.close(); 1505 } 1506 } 1507 } 1508 } 1509 processDirectory(String path, String extensions, MediaScannerClient client)1510 private native void processDirectory(String path, String extensions, MediaScannerClient client); processFile(String path, String mimeType, MediaScannerClient client)1511 private native void processFile(String path, String mimeType, MediaScannerClient client); setLocale(String locale)1512 public native void setLocale(String locale); 1513 extractAlbumArt(FileDescriptor fd)1514 public native byte[] extractAlbumArt(FileDescriptor fd); 1515 native_init()1516 private static native final void native_init(); native_setup()1517 private native final void native_setup(); native_finalize()1518 private native final void native_finalize(); 1519 @Override finalize()1520 protected void finalize() { 1521 mContext.getContentResolver().releaseProvider(mMediaProvider); 1522 native_finalize(); 1523 } 1524 } 1525