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