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