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