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