1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.media; 18 19 import android.content.ContentProviderClient; 20 import android.content.ContentResolver; 21 import android.content.ContentUris; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.database.Cursor; 25 import android.database.SQLException; 26 import android.drm.DrmManagerClient; 27 import android.graphics.BitmapFactory; 28 import android.mtp.MtpConstants; 29 import android.net.Uri; 30 import android.os.Environment; 31 import android.os.RemoteException; 32 import android.os.SystemProperties; 33 import android.provider.MediaStore; 34 import android.provider.MediaStore.Audio; 35 import android.provider.MediaStore.Audio.Playlists; 36 import android.provider.MediaStore.Files; 37 import android.provider.MediaStore.Files.FileColumns; 38 import android.provider.MediaStore.Images; 39 import android.provider.MediaStore.Video; 40 import android.provider.Settings; 41 import android.provider.Settings.SettingNotFoundException; 42 import android.sax.Element; 43 import android.sax.ElementListener; 44 import android.sax.RootElement; 45 import android.system.ErrnoException; 46 import android.system.Os; 47 import android.text.TextUtils; 48 import android.util.Log; 49 import android.util.Xml; 50 51 import dalvik.system.CloseGuard; 52 53 import org.xml.sax.Attributes; 54 import org.xml.sax.ContentHandler; 55 import org.xml.sax.SAXException; 56 57 import java.io.BufferedReader; 58 import java.io.File; 59 import java.io.FileDescriptor; 60 import java.io.FileInputStream; 61 import java.io.IOException; 62 import java.io.InputStreamReader; 63 import java.util.ArrayList; 64 import java.util.HashMap; 65 import java.util.HashSet; 66 import java.util.Iterator; 67 import java.util.Locale; 68 import java.util.concurrent.atomic.AtomicBoolean; 69 70 /** 71 * Internal service helper that no-one should use directly. 72 * 73 * The way the scan currently works is: 74 * - The Java MediaScannerService creates a MediaScanner (this class), and calls 75 * MediaScanner.scanDirectories on it. 76 * - scanDirectories() calls the native processDirectory() for each of the specified directories. 77 * - the processDirectory() JNI method wraps the provided mediascanner client in a native 78 * 'MyMediaScannerClient' class, then calls processDirectory() on the native MediaScanner 79 * object (which got created when the Java MediaScanner was created). 80 * - native MediaScanner.processDirectory() calls 81 * doProcessDirectory(), which recurses over the folder, and calls 82 * native MyMediaScannerClient.scanFile() for every file whose extension matches. 83 * - native MyMediaScannerClient.scanFile() calls back on Java MediaScannerClient.scanFile, 84 * which calls doScanFile, which after some setup calls back down to native code, calling 85 * MediaScanner.processFile(). 86 * - MediaScanner.processFile() calls one of several methods, depending on the type of the 87 * file: parseMP3, parseMP4, parseMidi, parseOgg or parseWMA. 88 * - each of these methods gets metadata key/value pairs from the file, and repeatedly 89 * calls native MyMediaScannerClient.handleStringTag, which calls back up to its Java 90 * counterparts in this file. 91 * - Java handleStringTag() gathers the key/value pairs that it's interested in. 92 * - once processFile returns and we're back in Java code in doScanFile(), it calls 93 * Java MyMediaScannerClient.endFile(), which takes all the data that's been 94 * gathered and inserts an entry in to the database. 95 * 96 * In summary: 97 * Java MediaScannerService calls 98 * Java MediaScanner scanDirectories, which calls 99 * Java MediaScanner processDirectory (native method), which calls 100 * native MediaScanner processDirectory, which calls 101 * native MyMediaScannerClient scanFile, which calls 102 * Java MyMediaScannerClient scanFile, which calls 103 * Java MediaScannerClient doScanFile, which calls 104 * Java MediaScanner processFile (native method), which calls 105 * native MediaScanner processFile, which calls 106 * native parseMP3, parseMP4, parseMidi, parseOgg or parseWMA, which calls 107 * native MyMediaScanner handleStringTag, which calls 108 * Java MyMediaScanner handleStringTag. 109 * Once MediaScanner processFile returns, an entry is inserted in to the database. 110 * 111 * The MediaScanner class is not thread-safe, so it should only be used in a single threaded manner. 112 * 113 * {@hide} 114 */ 115 public class MediaScanner implements AutoCloseable { 116 static { 117 System.loadLibrary("media_jni"); native_init()118 native_init(); 119 } 120 121 private final static String TAG = "MediaScanner"; 122 123 private static final String[] FILES_PRESCAN_PROJECTION = new String[] { 124 Files.FileColumns._ID, // 0 125 Files.FileColumns.DATA, // 1 126 Files.FileColumns.FORMAT, // 2 127 Files.FileColumns.DATE_MODIFIED, // 3 128 }; 129 130 private static final String[] ID_PROJECTION = new String[] { 131 Files.FileColumns._ID, 132 }; 133 134 private static final int FILES_PRESCAN_ID_COLUMN_INDEX = 0; 135 private static final int FILES_PRESCAN_PATH_COLUMN_INDEX = 1; 136 private static final int FILES_PRESCAN_FORMAT_COLUMN_INDEX = 2; 137 private static final int FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX = 3; 138 139 private static final String[] PLAYLIST_MEMBERS_PROJECTION = new String[] { 140 Audio.Playlists.Members.PLAYLIST_ID, // 0 141 }; 142 143 private static final int ID_PLAYLISTS_COLUMN_INDEX = 0; 144 private static final int PATH_PLAYLISTS_COLUMN_INDEX = 1; 145 private static final int DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX = 2; 146 147 private static final String RINGTONES_DIR = "/ringtones/"; 148 private static final String NOTIFICATIONS_DIR = "/notifications/"; 149 private static final String ALARMS_DIR = "/alarms/"; 150 private static final String MUSIC_DIR = "/music/"; 151 private static final String PODCAST_DIR = "/podcasts/"; 152 153 private static final String[] ID3_GENRES = { 154 // ID3v1 Genres 155 "Blues", 156 "Classic Rock", 157 "Country", 158 "Dance", 159 "Disco", 160 "Funk", 161 "Grunge", 162 "Hip-Hop", 163 "Jazz", 164 "Metal", 165 "New Age", 166 "Oldies", 167 "Other", 168 "Pop", 169 "R&B", 170 "Rap", 171 "Reggae", 172 "Rock", 173 "Techno", 174 "Industrial", 175 "Alternative", 176 "Ska", 177 "Death Metal", 178 "Pranks", 179 "Soundtrack", 180 "Euro-Techno", 181 "Ambient", 182 "Trip-Hop", 183 "Vocal", 184 "Jazz+Funk", 185 "Fusion", 186 "Trance", 187 "Classical", 188 "Instrumental", 189 "Acid", 190 "House", 191 "Game", 192 "Sound Clip", 193 "Gospel", 194 "Noise", 195 "AlternRock", 196 "Bass", 197 "Soul", 198 "Punk", 199 "Space", 200 "Meditative", 201 "Instrumental Pop", 202 "Instrumental Rock", 203 "Ethnic", 204 "Gothic", 205 "Darkwave", 206 "Techno-Industrial", 207 "Electronic", 208 "Pop-Folk", 209 "Eurodance", 210 "Dream", 211 "Southern Rock", 212 "Comedy", 213 "Cult", 214 "Gangsta", 215 "Top 40", 216 "Christian Rap", 217 "Pop/Funk", 218 "Jungle", 219 "Native American", 220 "Cabaret", 221 "New Wave", 222 "Psychadelic", 223 "Rave", 224 "Showtunes", 225 "Trailer", 226 "Lo-Fi", 227 "Tribal", 228 "Acid Punk", 229 "Acid Jazz", 230 "Polka", 231 "Retro", 232 "Musical", 233 "Rock & Roll", 234 "Hard Rock", 235 // The following genres are Winamp extensions 236 "Folk", 237 "Folk-Rock", 238 "National Folk", 239 "Swing", 240 "Fast Fusion", 241 "Bebob", 242 "Latin", 243 "Revival", 244 "Celtic", 245 "Bluegrass", 246 "Avantgarde", 247 "Gothic Rock", 248 "Progressive Rock", 249 "Psychedelic Rock", 250 "Symphonic Rock", 251 "Slow Rock", 252 "Big Band", 253 "Chorus", 254 "Easy Listening", 255 "Acoustic", 256 "Humour", 257 "Speech", 258 "Chanson", 259 "Opera", 260 "Chamber Music", 261 "Sonata", 262 "Symphony", 263 "Booty Bass", 264 "Primus", 265 "Porn Groove", 266 "Satire", 267 "Slow Jam", 268 "Club", 269 "Tango", 270 "Samba", 271 "Folklore", 272 "Ballad", 273 "Power Ballad", 274 "Rhythmic Soul", 275 "Freestyle", 276 "Duet", 277 "Punk Rock", 278 "Drum Solo", 279 "A capella", 280 "Euro-House", 281 "Dance Hall", 282 // The following ones seem to be fairly widely supported as well 283 "Goa", 284 "Drum & Bass", 285 "Club-House", 286 "Hardcore", 287 "Terror", 288 "Indie", 289 "Britpop", 290 null, 291 "Polsk Punk", 292 "Beat", 293 "Christian Gangsta", 294 "Heavy Metal", 295 "Black Metal", 296 "Crossover", 297 "Contemporary Christian", 298 "Christian Rock", 299 "Merengue", 300 "Salsa", 301 "Thrash Metal", 302 "Anime", 303 "JPop", 304 "Synthpop", 305 // 148 and up don't seem to have been defined yet. 306 }; 307 308 private long mNativeContext; 309 private final Context mContext; 310 private final String mPackageName; 311 private final String mVolumeName; 312 private final ContentProviderClient mMediaProvider; 313 private final Uri mAudioUri; 314 private final Uri mVideoUri; 315 private final Uri mImagesUri; 316 private final Uri mThumbsUri; 317 private final Uri mPlaylistsUri; 318 private final Uri mFilesUri; 319 private final Uri mFilesUriNoNotify; 320 private final boolean mProcessPlaylists; 321 private final boolean mProcessGenres; 322 private int mMtpObjectHandle; 323 324 private final AtomicBoolean mClosed = new AtomicBoolean(); 325 private final CloseGuard mCloseGuard = CloseGuard.get(); 326 327 /** whether to use bulk inserts or individual inserts for each item */ 328 private static final boolean ENABLE_BULK_INSERTS = true; 329 330 // used when scanning the image database so we know whether we have to prune 331 // old thumbnail files 332 private int mOriginalCount; 333 /** Whether the scanner has set a default sound for the ringer ringtone. */ 334 private boolean mDefaultRingtoneSet; 335 /** Whether the scanner has set a default sound for the notification ringtone. */ 336 private boolean mDefaultNotificationSet; 337 /** Whether the scanner has set a default sound for the alarm ringtone. */ 338 private boolean mDefaultAlarmSet; 339 /** The filename for the default sound for the ringer ringtone. */ 340 private String mDefaultRingtoneFilename; 341 /** The filename for the default sound for the notification ringtone. */ 342 private String mDefaultNotificationFilename; 343 /** The filename for the default sound for the alarm ringtone. */ 344 private String mDefaultAlarmAlertFilename; 345 /** 346 * The prefix for system properties that define the default sound for 347 * ringtones. Concatenate the name of the setting from Settings 348 * to get the full system property. 349 */ 350 private static final String DEFAULT_RINGTONE_PROPERTY_PREFIX = "ro.config."; 351 352 private final BitmapFactory.Options mBitmapOptions = new BitmapFactory.Options(); 353 354 private static class FileEntry { 355 long mRowId; 356 String mPath; 357 long mLastModified; 358 int mFormat; 359 boolean mLastModifiedChanged; 360 FileEntry(long rowId, String path, long lastModified, int format)361 FileEntry(long rowId, String path, long lastModified, int format) { 362 mRowId = rowId; 363 mPath = path; 364 mLastModified = lastModified; 365 mFormat = format; 366 mLastModifiedChanged = false; 367 } 368 369 @Override toString()370 public String toString() { 371 return mPath + " mRowId: " + mRowId; 372 } 373 } 374 375 private static class PlaylistEntry { 376 String path; 377 long bestmatchid; 378 int bestmatchlevel; 379 } 380 381 private final ArrayList<PlaylistEntry> mPlaylistEntries = new ArrayList<>(); 382 private final ArrayList<FileEntry> mPlayLists = new ArrayList<>(); 383 384 private MediaInserter mMediaInserter; 385 386 private DrmManagerClient mDrmManagerClient = null; 387 MediaScanner(Context c, String volumeName)388 public MediaScanner(Context c, String volumeName) { 389 native_setup(); 390 mContext = c; 391 mPackageName = c.getPackageName(); 392 mVolumeName = volumeName; 393 394 mBitmapOptions.inSampleSize = 1; 395 mBitmapOptions.inJustDecodeBounds = true; 396 397 setDefaultRingtoneFileNames(); 398 399 mMediaProvider = mContext.getContentResolver() 400 .acquireContentProviderClient(MediaStore.AUTHORITY); 401 402 mAudioUri = Audio.Media.getContentUri(volumeName); 403 mVideoUri = Video.Media.getContentUri(volumeName); 404 mImagesUri = Images.Media.getContentUri(volumeName); 405 mThumbsUri = Images.Thumbnails.getContentUri(volumeName); 406 mFilesUri = Files.getContentUri(volumeName); 407 mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build(); 408 409 if (!volumeName.equals("internal")) { 410 // we only support playlists on external media 411 mProcessPlaylists = true; 412 mProcessGenres = true; 413 mPlaylistsUri = Playlists.getContentUri(volumeName); 414 } else { 415 mProcessPlaylists = false; 416 mProcessGenres = false; 417 mPlaylistsUri = null; 418 } 419 420 final Locale locale = mContext.getResources().getConfiguration().locale; 421 if (locale != null) { 422 String language = locale.getLanguage(); 423 String country = locale.getCountry(); 424 if (language != null) { 425 if (country != null) { 426 setLocale(language + "_" + country); 427 } else { 428 setLocale(language); 429 } 430 } 431 } 432 433 mCloseGuard.open("close"); 434 } 435 setDefaultRingtoneFileNames()436 private void setDefaultRingtoneFileNames() { 437 mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 438 + Settings.System.RINGTONE); 439 mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 440 + Settings.System.NOTIFICATION_SOUND); 441 mDefaultAlarmAlertFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 442 + Settings.System.ALARM_ALERT); 443 } 444 445 private final MyMediaScannerClient mClient = new MyMediaScannerClient(); 446 isDrmEnabled()447 private boolean isDrmEnabled() { 448 String prop = SystemProperties.get("drm.service.enabled"); 449 return prop != null && prop.equals("true"); 450 } 451 452 private class MyMediaScannerClient implements MediaScannerClient { 453 454 private String mArtist; 455 private String mAlbumArtist; // use this if mArtist is missing 456 private String mAlbum; 457 private String mTitle; 458 private String mComposer; 459 private String mGenre; 460 private String mMimeType; 461 private int mFileType; 462 private int mTrack; 463 private int mYear; 464 private int mDuration; 465 private String mPath; 466 private long mLastModified; 467 private long mFileSize; 468 private String mWriter; 469 private int mCompilation; 470 private boolean mIsDrm; 471 private boolean mNoMedia; // flag to suppress file from appearing in media tables 472 private int mWidth; 473 private int mHeight; 474 beginFile(String path, String mimeType, long lastModified, long fileSize, boolean isDirectory, boolean noMedia)475 public FileEntry beginFile(String path, String mimeType, long lastModified, 476 long fileSize, boolean isDirectory, boolean noMedia) { 477 mMimeType = mimeType; 478 mFileType = 0; 479 mFileSize = fileSize; 480 mIsDrm = false; 481 482 if (!isDirectory) { 483 if (!noMedia && isNoMediaFile(path)) { 484 noMedia = true; 485 } 486 mNoMedia = noMedia; 487 488 // try mimeType first, if it is specified 489 if (mimeType != null) { 490 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 491 } 492 493 // if mimeType was not specified, compute file type based on file extension. 494 if (mFileType == 0) { 495 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 496 if (mediaFileType != null) { 497 mFileType = mediaFileType.fileType; 498 if (mMimeType == null) { 499 mMimeType = mediaFileType.mimeType; 500 } 501 } 502 } 503 504 if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) { 505 mFileType = getFileTypeFromDrm(path); 506 } 507 } 508 509 FileEntry entry = makeEntryFor(path); 510 // add some slack to avoid a rounding error 511 long delta = (entry != null) ? (lastModified - entry.mLastModified) : 0; 512 boolean wasModified = delta > 1 || delta < -1; 513 if (entry == null || wasModified) { 514 if (wasModified) { 515 entry.mLastModified = lastModified; 516 } else { 517 entry = new FileEntry(0, path, lastModified, 518 (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0)); 519 } 520 entry.mLastModifiedChanged = true; 521 } 522 523 if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) { 524 mPlayLists.add(entry); 525 // we don't process playlists in the main scan, so return null 526 return null; 527 } 528 529 // clear all the metadata 530 mArtist = null; 531 mAlbumArtist = null; 532 mAlbum = null; 533 mTitle = null; 534 mComposer = null; 535 mGenre = null; 536 mTrack = 0; 537 mYear = 0; 538 mDuration = 0; 539 mPath = path; 540 mLastModified = lastModified; 541 mWriter = null; 542 mCompilation = 0; 543 mWidth = 0; 544 mHeight = 0; 545 546 return entry; 547 } 548 549 @Override 550 public void scanFile(String path, long lastModified, long fileSize, 551 boolean isDirectory, boolean noMedia) { 552 // This is the callback funtion from native codes. 553 // Log.v(TAG, "scanFile: "+path); 554 doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia); 555 } 556 557 public Uri doScanFile(String path, String mimeType, long lastModified, 558 long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) { 559 Uri result = null; 560 // long t1 = System.currentTimeMillis(); 561 try { 562 FileEntry entry = beginFile(path, mimeType, lastModified, 563 fileSize, isDirectory, noMedia); 564 565 if (entry == null) { 566 return null; 567 } 568 569 // if this file was just inserted via mtp, set the rowid to zero 570 // (even though it already exists in the database), to trigger 571 // the correct code path for updating its entry 572 if (mMtpObjectHandle != 0) { 573 entry.mRowId = 0; 574 } 575 576 if (entry.mPath != null && 577 ((!mDefaultNotificationSet && 578 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) 579 || (!mDefaultRingtoneSet && 580 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) 581 || (!mDefaultAlarmSet && 582 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)))) { 583 Log.w(TAG, "forcing rescan of " + entry.mPath + 584 "since ringtone setting didn't finish"); 585 scanAlways = true; 586 } 587 588 // rescan for metadata if file was modified since last scan 589 if (entry != null && (entry.mLastModifiedChanged || scanAlways)) { 590 if (noMedia) { 591 result = endFile(entry, false, false, false, false, false); 592 } else { 593 String lowpath = path.toLowerCase(Locale.ROOT); 594 boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0); 595 boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0); 596 boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0); 597 boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0); 598 boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) || 599 (!ringtones && !notifications && !alarms && !podcasts); 600 601 boolean isaudio = MediaFile.isAudioFileType(mFileType); 602 boolean isvideo = MediaFile.isVideoFileType(mFileType); 603 boolean isimage = MediaFile.isImageFileType(mFileType); 604 605 if (isaudio || isvideo || isimage) { 606 path = Environment.maybeTranslateEmulatedPathToInternal(new File(path)) 607 .getAbsolutePath(); 608 } 609 610 // we only extract metadata for audio and video files 611 if (isaudio || isvideo) { 612 processFile(path, mimeType, this); 613 } 614 615 if (isimage) { 616 processImageFile(path); 617 } 618 619 result = endFile(entry, ringtones, notifications, alarms, music, podcasts); 620 } 621 } 622 } catch (RemoteException e) { 623 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 624 } 625 // long t2 = System.currentTimeMillis(); 626 // Log.v(TAG, "scanFile: " + path + " took " + (t2-t1)); 627 return result; 628 } 629 630 private int parseSubstring(String s, int start, int defaultValue) { 631 int length = s.length(); 632 if (start == length) return defaultValue; 633 634 char ch = s.charAt(start++); 635 // return defaultValue if we have no integer at all 636 if (ch < '0' || ch > '9') return defaultValue; 637 638 int result = ch - '0'; 639 while (start < length) { 640 ch = s.charAt(start++); 641 if (ch < '0' || ch > '9') return result; 642 result = result * 10 + (ch - '0'); 643 } 644 645 return result; 646 } 647 648 public void handleStringTag(String name, String value) { 649 if (name.equalsIgnoreCase("title") || name.startsWith("title;")) { 650 // Don't trim() here, to preserve the special \001 character 651 // used to force sorting. The media provider will trim() before 652 // inserting the title in to the database. 653 mTitle = value; 654 } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) { 655 mArtist = value.trim(); 656 } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;") 657 || name.equalsIgnoreCase("band") || name.startsWith("band;")) { 658 mAlbumArtist = value.trim(); 659 } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) { 660 mAlbum = value.trim(); 661 } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) { 662 mComposer = value.trim(); 663 } else if (mProcessGenres && 664 (name.equalsIgnoreCase("genre") || name.startsWith("genre;"))) { 665 mGenre = getGenreName(value); 666 } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) { 667 mYear = parseSubstring(value, 0, 0); 668 } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) { 669 // track number might be of the form "2/12" 670 // we just read the number before the slash 671 int num = parseSubstring(value, 0, 0); 672 mTrack = (mTrack / 1000) * 1000 + num; 673 } else if (name.equalsIgnoreCase("discnumber") || 674 name.equals("set") || name.startsWith("set;")) { 675 // set number might be of the form "1/3" 676 // we just read the number before the slash 677 int num = parseSubstring(value, 0, 0); 678 mTrack = (num * 1000) + (mTrack % 1000); 679 } else if (name.equalsIgnoreCase("duration")) { 680 mDuration = parseSubstring(value, 0, 0); 681 } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) { 682 mWriter = value.trim(); 683 } else if (name.equalsIgnoreCase("compilation")) { 684 mCompilation = parseSubstring(value, 0, 0); 685 } else if (name.equalsIgnoreCase("isdrm")) { 686 mIsDrm = (parseSubstring(value, 0, 0) == 1); 687 } else if (name.equalsIgnoreCase("width")) { 688 mWidth = parseSubstring(value, 0, 0); 689 } else if (name.equalsIgnoreCase("height")) { 690 mHeight = parseSubstring(value, 0, 0); 691 } else { 692 //Log.v(TAG, "unknown tag: " + name + " (" + mProcessGenres + ")"); 693 } 694 } 695 696 private boolean convertGenreCode(String input, String expected) { 697 String output = getGenreName(input); 698 if (output.equals(expected)) { 699 return true; 700 } else { 701 Log.d(TAG, "'" + input + "' -> '" + output + "', expected '" + expected + "'"); 702 return false; 703 } 704 } 705 private void testGenreNameConverter() { 706 convertGenreCode("2", "Country"); 707 convertGenreCode("(2)", "Country"); 708 convertGenreCode("(2", "(2"); 709 convertGenreCode("2 Foo", "Country"); 710 convertGenreCode("(2) Foo", "Country"); 711 convertGenreCode("(2 Foo", "(2 Foo"); 712 convertGenreCode("2Foo", "2Foo"); 713 convertGenreCode("(2)Foo", "Country"); 714 convertGenreCode("200 Foo", "Foo"); 715 convertGenreCode("(200) Foo", "Foo"); 716 convertGenreCode("200Foo", "200Foo"); 717 convertGenreCode("(200)Foo", "Foo"); 718 convertGenreCode("200)Foo", "200)Foo"); 719 convertGenreCode("200) Foo", "200) Foo"); 720 } 721 722 public String getGenreName(String genreTagValue) { 723 724 if (genreTagValue == null) { 725 return null; 726 } 727 final int length = genreTagValue.length(); 728 729 if (length > 0) { 730 boolean parenthesized = false; 731 StringBuffer number = new StringBuffer(); 732 int i = 0; 733 for (; i < length; ++i) { 734 char c = genreTagValue.charAt(i); 735 if (i == 0 && c == '(') { 736 parenthesized = true; 737 } else if (Character.isDigit(c)) { 738 number.append(c); 739 } else { 740 break; 741 } 742 } 743 char charAfterNumber = i < length ? genreTagValue.charAt(i) : ' '; 744 if ((parenthesized && charAfterNumber == ')') 745 || !parenthesized && Character.isWhitespace(charAfterNumber)) { 746 try { 747 short genreIndex = Short.parseShort(number.toString()); 748 if (genreIndex >= 0) { 749 if (genreIndex < ID3_GENRES.length && ID3_GENRES[genreIndex] != null) { 750 return ID3_GENRES[genreIndex]; 751 } else if (genreIndex == 0xFF) { 752 return null; 753 } else if (genreIndex < 0xFF && (i + 1) < length) { 754 // genre is valid but unknown, 755 // if there is a string after the value we take it 756 if (parenthesized && charAfterNumber == ')') { 757 i++; 758 } 759 String ret = genreTagValue.substring(i).trim(); 760 if (ret.length() != 0) { 761 return ret; 762 } 763 } else { 764 // else return the number, without parentheses 765 return number.toString(); 766 } 767 } 768 } catch (NumberFormatException e) { 769 } 770 } 771 } 772 773 return genreTagValue; 774 } 775 776 private void processImageFile(String path) { 777 try { 778 mBitmapOptions.outWidth = 0; 779 mBitmapOptions.outHeight = 0; 780 BitmapFactory.decodeFile(path, mBitmapOptions); 781 mWidth = mBitmapOptions.outWidth; 782 mHeight = mBitmapOptions.outHeight; 783 } catch (Throwable th) { 784 // ignore; 785 } 786 } 787 788 public void setMimeType(String mimeType) { 789 if ("audio/mp4".equals(mMimeType) && 790 mimeType.startsWith("video")) { 791 // for feature parity with Donut, we force m4a files to keep the 792 // audio/mp4 mimetype, even if they are really "enhanced podcasts" 793 // with a video track 794 return; 795 } 796 mMimeType = mimeType; 797 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 798 } 799 800 /** 801 * Formats the data into a values array suitable for use with the Media 802 * Content Provider. 803 * 804 * @return a map of values 805 */ 806 private ContentValues toValues() { 807 ContentValues map = new ContentValues(); 808 809 map.put(MediaStore.MediaColumns.DATA, mPath); 810 map.put(MediaStore.MediaColumns.TITLE, mTitle); 811 map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified); 812 map.put(MediaStore.MediaColumns.SIZE, mFileSize); 813 map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType); 814 map.put(MediaStore.MediaColumns.IS_DRM, mIsDrm); 815 816 String resolution = null; 817 if (mWidth > 0 && mHeight > 0) { 818 map.put(MediaStore.MediaColumns.WIDTH, mWidth); 819 map.put(MediaStore.MediaColumns.HEIGHT, mHeight); 820 resolution = mWidth + "x" + mHeight; 821 } 822 823 if (!mNoMedia) { 824 if (MediaFile.isVideoFileType(mFileType)) { 825 map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 826 ? mArtist : MediaStore.UNKNOWN_STRING)); 827 map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 828 ? mAlbum : MediaStore.UNKNOWN_STRING)); 829 map.put(Video.Media.DURATION, mDuration); 830 if (resolution != null) { 831 map.put(Video.Media.RESOLUTION, resolution); 832 } 833 } else if (MediaFile.isImageFileType(mFileType)) { 834 // FIXME - add DESCRIPTION 835 } else if (MediaFile.isAudioFileType(mFileType)) { 836 map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ? 837 mArtist : MediaStore.UNKNOWN_STRING); 838 map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null && 839 mAlbumArtist.length() > 0) ? mAlbumArtist : null); 840 map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ? 841 mAlbum : MediaStore.UNKNOWN_STRING); 842 map.put(Audio.Media.COMPOSER, mComposer); 843 map.put(Audio.Media.GENRE, mGenre); 844 if (mYear != 0) { 845 map.put(Audio.Media.YEAR, mYear); 846 } 847 map.put(Audio.Media.TRACK, mTrack); 848 map.put(Audio.Media.DURATION, mDuration); 849 map.put(Audio.Media.COMPILATION, mCompilation); 850 } 851 } 852 return map; 853 } 854 855 private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications, 856 boolean alarms, boolean music, boolean podcasts) 857 throws RemoteException { 858 // update database 859 860 // use album artist if artist is missing 861 if (mArtist == null || mArtist.length() == 0) { 862 mArtist = mAlbumArtist; 863 } 864 865 ContentValues values = toValues(); 866 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 867 if (title == null || TextUtils.isEmpty(title.trim())) { 868 title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA)); 869 values.put(MediaStore.MediaColumns.TITLE, title); 870 } 871 String album = values.getAsString(Audio.Media.ALBUM); 872 if (MediaStore.UNKNOWN_STRING.equals(album)) { 873 album = values.getAsString(MediaStore.MediaColumns.DATA); 874 // extract last path segment before file name 875 int lastSlash = album.lastIndexOf('/'); 876 if (lastSlash >= 0) { 877 int previousSlash = 0; 878 while (true) { 879 int idx = album.indexOf('/', previousSlash + 1); 880 if (idx < 0 || idx >= lastSlash) { 881 break; 882 } 883 previousSlash = idx; 884 } 885 if (previousSlash != 0) { 886 album = album.substring(previousSlash + 1, lastSlash); 887 values.put(Audio.Media.ALBUM, album); 888 } 889 } 890 } 891 long rowId = entry.mRowId; 892 if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) { 893 // Only set these for new entries. For existing entries, they 894 // may have been modified later, and we want to keep the current 895 // values so that custom ringtones still show up in the ringtone 896 // picker. 897 values.put(Audio.Media.IS_RINGTONE, ringtones); 898 values.put(Audio.Media.IS_NOTIFICATION, notifications); 899 values.put(Audio.Media.IS_ALARM, alarms); 900 values.put(Audio.Media.IS_MUSIC, music); 901 values.put(Audio.Media.IS_PODCAST, podcasts); 902 } else if ((mFileType == MediaFile.FILE_TYPE_JPEG 903 || MediaFile.isRawImageFileType(mFileType)) && !mNoMedia) { 904 ExifInterface exif = null; 905 try { 906 exif = new ExifInterface(entry.mPath); 907 } catch (IOException ex) { 908 // exif is null 909 } 910 if (exif != null) { 911 float[] latlng = new float[2]; 912 if (exif.getLatLong(latlng)) { 913 values.put(Images.Media.LATITUDE, latlng[0]); 914 values.put(Images.Media.LONGITUDE, latlng[1]); 915 } 916 917 long time = exif.getGpsDateTime(); 918 if (time != -1) { 919 values.put(Images.Media.DATE_TAKEN, time); 920 } else { 921 // If no time zone information is available, we should consider using 922 // EXIF local time as taken time if the difference between file time 923 // and EXIF local time is not less than 1 Day, otherwise MediaProvider 924 // will use file time as taken time. 925 time = exif.getDateTime(); 926 if (time != -1 && Math.abs(mLastModified * 1000 - time) >= 86400000) { 927 values.put(Images.Media.DATE_TAKEN, time); 928 } 929 } 930 931 int orientation = exif.getAttributeInt( 932 ExifInterface.TAG_ORIENTATION, -1); 933 if (orientation != -1) { 934 // We only recognize a subset of orientation tag values. 935 int degree; 936 switch(orientation) { 937 case ExifInterface.ORIENTATION_ROTATE_90: 938 degree = 90; 939 break; 940 case ExifInterface.ORIENTATION_ROTATE_180: 941 degree = 180; 942 break; 943 case ExifInterface.ORIENTATION_ROTATE_270: 944 degree = 270; 945 break; 946 default: 947 degree = 0; 948 break; 949 } 950 values.put(Images.Media.ORIENTATION, degree); 951 } 952 } 953 } 954 955 Uri tableUri = mFilesUri; 956 MediaInserter inserter = mMediaInserter; 957 if (!mNoMedia) { 958 if (MediaFile.isVideoFileType(mFileType)) { 959 tableUri = mVideoUri; 960 } else if (MediaFile.isImageFileType(mFileType)) { 961 tableUri = mImagesUri; 962 } else if (MediaFile.isAudioFileType(mFileType)) { 963 tableUri = mAudioUri; 964 } 965 } 966 Uri result = null; 967 boolean needToSetSettings = false; 968 // Setting a flag in order not to use bulk insert for the file related with 969 // notifications, ringtones, and alarms, because the rowId of the inserted file is 970 // needed. 971 if (notifications && !mDefaultNotificationSet) { 972 if (TextUtils.isEmpty(mDefaultNotificationFilename) || 973 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) { 974 needToSetSettings = true; 975 } 976 } else if (ringtones && !mDefaultRingtoneSet) { 977 if (TextUtils.isEmpty(mDefaultRingtoneFilename) || 978 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) { 979 needToSetSettings = true; 980 } 981 } else if (alarms && !mDefaultAlarmSet) { 982 if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) || 983 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) { 984 needToSetSettings = true; 985 } 986 } 987 988 if (rowId == 0) { 989 if (mMtpObjectHandle != 0) { 990 values.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle); 991 } 992 if (tableUri == mFilesUri) { 993 int format = entry.mFormat; 994 if (format == 0) { 995 format = MediaFile.getFormatCode(entry.mPath, mMimeType); 996 } 997 values.put(Files.FileColumns.FORMAT, format); 998 } 999 // New file, insert it. 1000 // Directories need to be inserted before the files they contain, so they 1001 // get priority when bulk inserting. 1002 // If the rowId of the inserted file is needed, it gets inserted immediately, 1003 // bypassing the bulk inserter. 1004 if (inserter == null || needToSetSettings) { 1005 if (inserter != null) { 1006 inserter.flushAll(); 1007 } 1008 result = mMediaProvider.insert(tableUri, values); 1009 } else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) { 1010 inserter.insertwithPriority(tableUri, values); 1011 } else { 1012 inserter.insert(tableUri, values); 1013 } 1014 1015 if (result != null) { 1016 rowId = ContentUris.parseId(result); 1017 entry.mRowId = rowId; 1018 } 1019 } else { 1020 // updated file 1021 result = ContentUris.withAppendedId(tableUri, rowId); 1022 // path should never change, and we want to avoid replacing mixed cased paths 1023 // with squashed lower case paths 1024 values.remove(MediaStore.MediaColumns.DATA); 1025 1026 int mediaType = 0; 1027 if (!MediaScanner.isNoMediaPath(entry.mPath)) { 1028 int fileType = MediaFile.getFileTypeForMimeType(mMimeType); 1029 if (MediaFile.isAudioFileType(fileType)) { 1030 mediaType = FileColumns.MEDIA_TYPE_AUDIO; 1031 } else if (MediaFile.isVideoFileType(fileType)) { 1032 mediaType = FileColumns.MEDIA_TYPE_VIDEO; 1033 } else if (MediaFile.isImageFileType(fileType)) { 1034 mediaType = FileColumns.MEDIA_TYPE_IMAGE; 1035 } else if (MediaFile.isPlayListFileType(fileType)) { 1036 mediaType = FileColumns.MEDIA_TYPE_PLAYLIST; 1037 } 1038 values.put(FileColumns.MEDIA_TYPE, mediaType); 1039 } 1040 mMediaProvider.update(result, values, null, null); 1041 } 1042 1043 if(needToSetSettings) { 1044 if (notifications) { 1045 setRingtoneIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId); 1046 mDefaultNotificationSet = true; 1047 } else if (ringtones) { 1048 setRingtoneIfNotSet(Settings.System.RINGTONE, tableUri, rowId); 1049 mDefaultRingtoneSet = true; 1050 } else if (alarms) { 1051 setRingtoneIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId); 1052 mDefaultAlarmSet = true; 1053 } 1054 } 1055 1056 return result; 1057 } 1058 1059 private boolean doesPathHaveFilename(String path, String filename) { 1060 int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1; 1061 int filenameLength = filename.length(); 1062 return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) && 1063 pathFilenameStart + filenameLength == path.length(); 1064 } 1065 1066 private void setRingtoneIfNotSet(String settingName, Uri uri, long rowId) { 1067 if (wasRingtoneAlreadySet(settingName)) { 1068 return; 1069 } 1070 1071 ContentResolver cr = mContext.getContentResolver(); 1072 String existingSettingValue = Settings.System.getString(cr, settingName); 1073 if (TextUtils.isEmpty(existingSettingValue)) { 1074 final Uri settingUri = Settings.System.getUriFor(settingName); 1075 final Uri ringtoneUri = ContentUris.withAppendedId(uri, rowId); 1076 RingtoneManager.setActualDefaultRingtoneUri(mContext, 1077 RingtoneManager.getDefaultType(settingUri), ringtoneUri); 1078 } 1079 Settings.System.putInt(cr, settingSetIndicatorName(settingName), 1); 1080 } 1081 1082 private int getFileTypeFromDrm(String path) { 1083 if (!isDrmEnabled()) { 1084 return 0; 1085 } 1086 1087 int resultFileType = 0; 1088 1089 if (mDrmManagerClient == null) { 1090 mDrmManagerClient = new DrmManagerClient(mContext); 1091 } 1092 1093 if (mDrmManagerClient.canHandle(path, null)) { 1094 mIsDrm = true; 1095 String drmMimetype = mDrmManagerClient.getOriginalMimeType(path); 1096 if (drmMimetype != null) { 1097 mMimeType = drmMimetype; 1098 resultFileType = MediaFile.getFileTypeForMimeType(drmMimetype); 1099 } 1100 } 1101 return resultFileType; 1102 } 1103 1104 }; // end of anonymous MediaScannerClient instance 1105 1106 private String settingSetIndicatorName(String base) { 1107 return base + "_set"; 1108 } 1109 1110 private boolean wasRingtoneAlreadySet(String name) { 1111 ContentResolver cr = mContext.getContentResolver(); 1112 String indicatorName = settingSetIndicatorName(name); 1113 try { 1114 return Settings.System.getInt(cr, indicatorName) != 0; 1115 } catch (SettingNotFoundException e) { 1116 return false; 1117 } 1118 } 1119 1120 private void prescan(String filePath, boolean prescanFiles) throws RemoteException { 1121 Cursor c = null; 1122 String where = null; 1123 String[] selectionArgs = null; 1124 1125 mPlayLists.clear(); 1126 1127 if (filePath != null) { 1128 // query for only one file 1129 where = MediaStore.Files.FileColumns._ID + ">?" + 1130 " AND " + Files.FileColumns.DATA + "=?"; 1131 selectionArgs = new String[] { "", filePath }; 1132 } else { 1133 where = MediaStore.Files.FileColumns._ID + ">?"; 1134 selectionArgs = new String[] { "" }; 1135 } 1136 1137 mDefaultRingtoneSet = wasRingtoneAlreadySet(Settings.System.RINGTONE); 1138 mDefaultNotificationSet = wasRingtoneAlreadySet(Settings.System.NOTIFICATION_SOUND); 1139 mDefaultAlarmSet = wasRingtoneAlreadySet(Settings.System.ALARM_ALERT); 1140 1141 // Tell the provider to not delete the file. 1142 // If the file is truly gone the delete is unnecessary, and we want to avoid 1143 // accidentally deleting files that are really there (this may happen if the 1144 // filesystem is mounted and unmounted while the scanner is running). 1145 Uri.Builder builder = mFilesUri.buildUpon(); 1146 builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false"); 1147 MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, builder.build()); 1148 1149 // Build the list of files from the content provider 1150 try { 1151 if (prescanFiles) { 1152 // First read existing files from the files table. 1153 // Because we'll be deleting entries for missing files as we go, 1154 // we need to query the database in small batches, to avoid problems 1155 // with CursorWindow positioning. 1156 long lastId = Long.MIN_VALUE; 1157 Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build(); 1158 1159 while (true) { 1160 selectionArgs[0] = "" + lastId; 1161 if (c != null) { 1162 c.close(); 1163 c = null; 1164 } 1165 c = mMediaProvider.query(limitUri, FILES_PRESCAN_PROJECTION, 1166 where, selectionArgs, MediaStore.Files.FileColumns._ID, null); 1167 if (c == null) { 1168 break; 1169 } 1170 1171 int num = c.getCount(); 1172 1173 if (num == 0) { 1174 break; 1175 } 1176 while (c.moveToNext()) { 1177 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1178 String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); 1179 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1180 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1181 lastId = rowId; 1182 1183 // Only consider entries with absolute path names. 1184 // This allows storing URIs in the database without the 1185 // media scanner removing them. 1186 if (path != null && path.startsWith("/")) { 1187 boolean exists = false; 1188 try { 1189 exists = Os.access(path, android.system.OsConstants.F_OK); 1190 } catch (ErrnoException e1) { 1191 } 1192 if (!exists && !MtpConstants.isAbstractObject(format)) { 1193 // do not delete missing playlists, since they may have been 1194 // modified by the user. 1195 // The user can delete them in the media player instead. 1196 // instead, clear the path and lastModified fields in the row 1197 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1198 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1199 1200 if (!MediaFile.isPlayListFileType(fileType)) { 1201 deleter.delete(rowId); 1202 if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) { 1203 deleter.flush(); 1204 String parent = new File(path).getParent(); 1205 mMediaProvider.call(MediaStore.UNHIDE_CALL, parent, null); 1206 } 1207 } 1208 } 1209 } 1210 } 1211 } 1212 } 1213 } 1214 finally { 1215 if (c != null) { 1216 c.close(); 1217 } 1218 deleter.flush(); 1219 } 1220 1221 // compute original size of images 1222 mOriginalCount = 0; 1223 c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null, null); 1224 if (c != null) { 1225 mOriginalCount = c.getCount(); 1226 c.close(); 1227 } 1228 } 1229 1230 private boolean inScanDirectory(String path, String[] directories) { 1231 for (int i = 0; i < directories.length; i++) { 1232 String directory = directories[i]; 1233 if (path.startsWith(directory)) { 1234 return true; 1235 } 1236 } 1237 return false; 1238 } 1239 1240 private void pruneDeadThumbnailFiles() { 1241 HashSet<String> existingFiles = new HashSet<String>(); 1242 String directory = "/sdcard/DCIM/.thumbnails"; 1243 String [] files = (new File(directory)).list(); 1244 Cursor c = null; 1245 if (files == null) 1246 files = new String[0]; 1247 1248 for (int i = 0; i < files.length; i++) { 1249 String fullPathString = directory + "/" + files[i]; 1250 existingFiles.add(fullPathString); 1251 } 1252 1253 try { 1254 c = mMediaProvider.query( 1255 mThumbsUri, 1256 new String [] { "_data" }, 1257 null, 1258 null, 1259 null, null); 1260 Log.v(TAG, "pruneDeadThumbnailFiles... " + c); 1261 if (c != null && c.moveToFirst()) { 1262 do { 1263 String fullPathString = c.getString(0); 1264 existingFiles.remove(fullPathString); 1265 } while (c.moveToNext()); 1266 } 1267 1268 for (String fileToDelete : existingFiles) { 1269 if (false) 1270 Log.v(TAG, "fileToDelete is " + fileToDelete); 1271 try { 1272 (new File(fileToDelete)).delete(); 1273 } catch (SecurityException ex) { 1274 } 1275 } 1276 1277 Log.v(TAG, "/pruneDeadThumbnailFiles... " + c); 1278 } catch (RemoteException e) { 1279 // We will soon be killed... 1280 } finally { 1281 if (c != null) { 1282 c.close(); 1283 } 1284 } 1285 } 1286 1287 static class MediaBulkDeleter { 1288 StringBuilder whereClause = new StringBuilder(); 1289 ArrayList<String> whereArgs = new ArrayList<String>(100); 1290 final ContentProviderClient mProvider; 1291 final Uri mBaseUri; 1292 1293 public MediaBulkDeleter(ContentProviderClient provider, Uri baseUri) { 1294 mProvider = provider; 1295 mBaseUri = baseUri; 1296 } 1297 1298 public void delete(long id) throws RemoteException { 1299 if (whereClause.length() != 0) { 1300 whereClause.append(","); 1301 } 1302 whereClause.append("?"); 1303 whereArgs.add("" + id); 1304 if (whereArgs.size() > 100) { 1305 flush(); 1306 } 1307 } 1308 public void flush() throws RemoteException { 1309 int size = whereArgs.size(); 1310 if (size > 0) { 1311 String [] foo = new String [size]; 1312 foo = whereArgs.toArray(foo); 1313 int numrows = mProvider.delete(mBaseUri, 1314 MediaStore.MediaColumns._ID + " IN (" + 1315 whereClause.toString() + ")", foo); 1316 //Log.i("@@@@@@@@@", "rows deleted: " + numrows); 1317 whereClause.setLength(0); 1318 whereArgs.clear(); 1319 } 1320 } 1321 } 1322 1323 private void postscan(final String[] directories) throws RemoteException { 1324 1325 // handle playlists last, after we know what media files are on the storage. 1326 if (mProcessPlaylists) { 1327 processPlayLists(); 1328 } 1329 1330 if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external"))) 1331 pruneDeadThumbnailFiles(); 1332 1333 // allow GC to clean up 1334 mPlayLists.clear(); 1335 } 1336 1337 private void releaseResources() { 1338 // release the DrmManagerClient resources 1339 if (mDrmManagerClient != null) { 1340 mDrmManagerClient.close(); 1341 mDrmManagerClient = null; 1342 } 1343 } 1344 1345 public void scanDirectories(String[] directories) { 1346 try { 1347 long start = System.currentTimeMillis(); 1348 prescan(null, true); 1349 long prescan = System.currentTimeMillis(); 1350 1351 if (ENABLE_BULK_INSERTS) { 1352 // create MediaInserter for bulk inserts 1353 mMediaInserter = new MediaInserter(mMediaProvider, 500); 1354 } 1355 1356 for (int i = 0; i < directories.length; i++) { 1357 processDirectory(directories[i], mClient); 1358 } 1359 1360 if (ENABLE_BULK_INSERTS) { 1361 // flush remaining inserts 1362 mMediaInserter.flushAll(); 1363 mMediaInserter = null; 1364 } 1365 1366 long scan = System.currentTimeMillis(); 1367 postscan(directories); 1368 long end = System.currentTimeMillis(); 1369 1370 if (false) { 1371 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1372 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1373 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1374 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1375 } 1376 } catch (SQLException e) { 1377 // this might happen if the SD card is removed while the media scanner is running 1378 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1379 } catch (UnsupportedOperationException e) { 1380 // this might happen if the SD card is removed while the media scanner is running 1381 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1382 } catch (RemoteException e) { 1383 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1384 } finally { 1385 releaseResources(); 1386 } 1387 } 1388 1389 // this function is used to scan a single file 1390 public Uri scanSingleFile(String path, String mimeType) { 1391 try { 1392 prescan(path, true); 1393 1394 File file = new File(path); 1395 if (!file.exists()) { 1396 return null; 1397 } 1398 1399 // lastModified is in milliseconds on Files. 1400 long lastModifiedSeconds = file.lastModified() / 1000; 1401 1402 // always scan the file, so we can return the content://media Uri for existing files 1403 return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(), 1404 false, true, MediaScanner.isNoMediaPath(path)); 1405 } catch (RemoteException e) { 1406 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1407 return null; 1408 } finally { 1409 releaseResources(); 1410 } 1411 } 1412 1413 private static boolean isNoMediaFile(String path) { 1414 File file = new File(path); 1415 if (file.isDirectory()) return false; 1416 1417 // special case certain file names 1418 // I use regionMatches() instead of substring() below 1419 // to avoid memory allocation 1420 int lastSlash = path.lastIndexOf('/'); 1421 if (lastSlash >= 0 && lastSlash + 2 < path.length()) { 1422 // ignore those ._* files created by MacOS 1423 if (path.regionMatches(lastSlash + 1, "._", 0, 2)) { 1424 return true; 1425 } 1426 1427 // ignore album art files created by Windows Media Player: 1428 // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg 1429 // and AlbumArt_{...}_Small.jpg 1430 if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) { 1431 if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) || 1432 path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) { 1433 return true; 1434 } 1435 int length = path.length() - lastSlash - 1; 1436 if ((length == 17 && path.regionMatches( 1437 true, lastSlash + 1, "AlbumArtSmall", 0, 13)) || 1438 (length == 10 1439 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) { 1440 return true; 1441 } 1442 } 1443 } 1444 return false; 1445 } 1446 1447 private static HashMap<String,String> mNoMediaPaths = new HashMap<String,String>(); 1448 private static HashMap<String,String> mMediaPaths = new HashMap<String,String>(); 1449 1450 /* MediaProvider calls this when a .nomedia file is added or removed */ 1451 public static void clearMediaPathCache(boolean clearMediaPaths, boolean clearNoMediaPaths) { 1452 synchronized (MediaScanner.class) { 1453 if (clearMediaPaths) { 1454 mMediaPaths.clear(); 1455 } 1456 if (clearNoMediaPaths) { 1457 mNoMediaPaths.clear(); 1458 } 1459 } 1460 } 1461 1462 public static boolean isNoMediaPath(String path) { 1463 if (path == null) { 1464 return false; 1465 } 1466 // return true if file or any parent directory has name starting with a dot 1467 if (path.indexOf("/.") >= 0) { 1468 return true; 1469 } 1470 1471 int firstSlash = path.lastIndexOf('/'); 1472 if (firstSlash <= 0) { 1473 return false; 1474 } 1475 String parent = path.substring(0, firstSlash); 1476 1477 synchronized (MediaScanner.class) { 1478 if (mNoMediaPaths.containsKey(parent)) { 1479 return true; 1480 } else if (!mMediaPaths.containsKey(parent)) { 1481 // check to see if any parent directories have a ".nomedia" file 1482 // start from 1 so we don't bother checking in the root directory 1483 int offset = 1; 1484 while (offset >= 0) { 1485 int slashIndex = path.indexOf('/', offset); 1486 if (slashIndex > offset) { 1487 slashIndex++; // move past slash 1488 File file = new File(path.substring(0, slashIndex) + ".nomedia"); 1489 if (file.exists()) { 1490 // we have a .nomedia in one of the parent directories 1491 mNoMediaPaths.put(parent, ""); 1492 return true; 1493 } 1494 } 1495 offset = slashIndex; 1496 } 1497 mMediaPaths.put(parent, ""); 1498 } 1499 } 1500 1501 return isNoMediaFile(path); 1502 } 1503 1504 public void scanMtpFile(String path, int objectHandle, int format) { 1505 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1506 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1507 File file = new File(path); 1508 long lastModifiedSeconds = file.lastModified() / 1000; 1509 1510 if (!MediaFile.isAudioFileType(fileType) && !MediaFile.isVideoFileType(fileType) && 1511 !MediaFile.isImageFileType(fileType) && !MediaFile.isPlayListFileType(fileType) && 1512 !MediaFile.isDrmFileType(fileType)) { 1513 1514 // no need to use the media scanner, but we need to update last modified and file size 1515 ContentValues values = new ContentValues(); 1516 values.put(Files.FileColumns.SIZE, file.length()); 1517 values.put(Files.FileColumns.DATE_MODIFIED, lastModifiedSeconds); 1518 try { 1519 String[] whereArgs = new String[] { Integer.toString(objectHandle) }; 1520 mMediaProvider.update(Files.getMtpObjectsUri(mVolumeName), values, 1521 "_id=?", whereArgs); 1522 } catch (RemoteException e) { 1523 Log.e(TAG, "RemoteException in scanMtpFile", e); 1524 } 1525 return; 1526 } 1527 1528 mMtpObjectHandle = objectHandle; 1529 Cursor fileList = null; 1530 try { 1531 if (MediaFile.isPlayListFileType(fileType)) { 1532 // build file cache so we can look up tracks in the playlist 1533 prescan(null, true); 1534 1535 FileEntry entry = makeEntryFor(path); 1536 if (entry != null) { 1537 fileList = mMediaProvider.query(mFilesUri, 1538 FILES_PRESCAN_PROJECTION, null, null, null, null); 1539 processPlayList(entry, fileList); 1540 } 1541 } else { 1542 // MTP will create a file entry for us so we don't want to do it in prescan 1543 prescan(path, false); 1544 1545 // always scan the file, so we can return the content://media Uri for existing files 1546 mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(), 1547 (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path)); 1548 } 1549 } catch (RemoteException e) { 1550 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1551 } finally { 1552 mMtpObjectHandle = 0; 1553 if (fileList != null) { 1554 fileList.close(); 1555 } 1556 releaseResources(); 1557 } 1558 } 1559 1560 FileEntry makeEntryFor(String path) { 1561 String where; 1562 String[] selectionArgs; 1563 1564 Cursor c = null; 1565 try { 1566 where = Files.FileColumns.DATA + "=?"; 1567 selectionArgs = new String[] { path }; 1568 c = mMediaProvider.query(mFilesUriNoNotify, FILES_PRESCAN_PROJECTION, 1569 where, selectionArgs, null, null); 1570 if (c.moveToFirst()) { 1571 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1572 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1573 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1574 return new FileEntry(rowId, path, lastModified, format); 1575 } 1576 } catch (RemoteException e) { 1577 } finally { 1578 if (c != null) { 1579 c.close(); 1580 } 1581 } 1582 return null; 1583 } 1584 1585 // returns the number of matching file/directory names, starting from the right 1586 private int matchPaths(String path1, String path2) { 1587 int result = 0; 1588 int end1 = path1.length(); 1589 int end2 = path2.length(); 1590 1591 while (end1 > 0 && end2 > 0) { 1592 int slash1 = path1.lastIndexOf('/', end1 - 1); 1593 int slash2 = path2.lastIndexOf('/', end2 - 1); 1594 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1595 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1596 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1597 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1598 if (start1 < 0) start1 = 0; else start1++; 1599 if (start2 < 0) start2 = 0; else start2++; 1600 int length = end1 - start1; 1601 if (end2 - start2 != length) break; 1602 if (path1.regionMatches(true, start1, path2, start2, length)) { 1603 result++; 1604 end1 = start1 - 1; 1605 end2 = start2 - 1; 1606 } else break; 1607 } 1608 1609 return result; 1610 } 1611 1612 private boolean matchEntries(long rowId, String data) { 1613 1614 int len = mPlaylistEntries.size(); 1615 boolean done = true; 1616 for (int i = 0; i < len; i++) { 1617 PlaylistEntry entry = mPlaylistEntries.get(i); 1618 if (entry.bestmatchlevel == Integer.MAX_VALUE) { 1619 continue; // this entry has been matched already 1620 } 1621 done = false; 1622 if (data.equalsIgnoreCase(entry.path)) { 1623 entry.bestmatchid = rowId; 1624 entry.bestmatchlevel = Integer.MAX_VALUE; 1625 continue; // no need for path matching 1626 } 1627 1628 int matchLength = matchPaths(data, entry.path); 1629 if (matchLength > entry.bestmatchlevel) { 1630 entry.bestmatchid = rowId; 1631 entry.bestmatchlevel = matchLength; 1632 } 1633 } 1634 return done; 1635 } 1636 1637 private void cachePlaylistEntry(String line, String playListDirectory) { 1638 PlaylistEntry entry = new PlaylistEntry(); 1639 // watch for trailing whitespace 1640 int entryLength = line.length(); 1641 while (entryLength > 0 && Character.isWhitespace(line.charAt(entryLength - 1))) entryLength--; 1642 // path should be longer than 3 characters. 1643 // avoid index out of bounds errors below by returning here. 1644 if (entryLength < 3) return; 1645 if (entryLength < line.length()) line = line.substring(0, entryLength); 1646 1647 // does entry appear to be an absolute path? 1648 // look for Unix or DOS absolute paths 1649 char ch1 = line.charAt(0); 1650 boolean fullPath = (ch1 == '/' || 1651 (Character.isLetter(ch1) && line.charAt(1) == ':' && line.charAt(2) == '\\')); 1652 // if we have a relative path, combine entry with playListDirectory 1653 if (!fullPath) 1654 line = playListDirectory + line; 1655 entry.path = line; 1656 //FIXME - should we look for "../" within the path? 1657 1658 mPlaylistEntries.add(entry); 1659 } 1660 1661 private void processCachedPlaylist(Cursor fileList, ContentValues values, Uri playlistUri) { 1662 fileList.moveToPosition(-1); 1663 while (fileList.moveToNext()) { 1664 long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1665 String data = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); 1666 if (matchEntries(rowId, data)) { 1667 break; 1668 } 1669 } 1670 1671 int len = mPlaylistEntries.size(); 1672 int index = 0; 1673 for (int i = 0; i < len; i++) { 1674 PlaylistEntry entry = mPlaylistEntries.get(i); 1675 if (entry.bestmatchlevel > 0) { 1676 try { 1677 values.clear(); 1678 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1679 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(entry.bestmatchid)); 1680 mMediaProvider.insert(playlistUri, values); 1681 index++; 1682 } catch (RemoteException e) { 1683 Log.e(TAG, "RemoteException in MediaScanner.processCachedPlaylist()", e); 1684 return; 1685 } 1686 } 1687 } 1688 mPlaylistEntries.clear(); 1689 } 1690 1691 private void processM3uPlayList(String path, String playListDirectory, Uri uri, 1692 ContentValues values, Cursor fileList) { 1693 BufferedReader reader = null; 1694 try { 1695 File f = new File(path); 1696 if (f.exists()) { 1697 reader = new BufferedReader( 1698 new InputStreamReader(new FileInputStream(f)), 8192); 1699 String line = reader.readLine(); 1700 mPlaylistEntries.clear(); 1701 while (line != null) { 1702 // ignore comment lines, which begin with '#' 1703 if (line.length() > 0 && line.charAt(0) != '#') { 1704 cachePlaylistEntry(line, playListDirectory); 1705 } 1706 line = reader.readLine(); 1707 } 1708 1709 processCachedPlaylist(fileList, values, uri); 1710 } 1711 } catch (IOException e) { 1712 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1713 } finally { 1714 try { 1715 if (reader != null) 1716 reader.close(); 1717 } catch (IOException e) { 1718 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1719 } 1720 } 1721 } 1722 1723 private void processPlsPlayList(String path, String playListDirectory, Uri uri, 1724 ContentValues values, Cursor fileList) { 1725 BufferedReader reader = null; 1726 try { 1727 File f = new File(path); 1728 if (f.exists()) { 1729 reader = new BufferedReader( 1730 new InputStreamReader(new FileInputStream(f)), 8192); 1731 String line = reader.readLine(); 1732 mPlaylistEntries.clear(); 1733 while (line != null) { 1734 // ignore comment lines, which begin with '#' 1735 if (line.startsWith("File")) { 1736 int equals = line.indexOf('='); 1737 if (equals > 0) { 1738 cachePlaylistEntry(line.substring(equals + 1), playListDirectory); 1739 } 1740 } 1741 line = reader.readLine(); 1742 } 1743 1744 processCachedPlaylist(fileList, values, uri); 1745 } 1746 } catch (IOException e) { 1747 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1748 } finally { 1749 try { 1750 if (reader != null) 1751 reader.close(); 1752 } catch (IOException e) { 1753 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1754 } 1755 } 1756 } 1757 1758 class WplHandler implements ElementListener { 1759 1760 final ContentHandler handler; 1761 String playListDirectory; 1762 1763 public WplHandler(String playListDirectory, Uri uri, Cursor fileList) { 1764 this.playListDirectory = playListDirectory; 1765 1766 RootElement root = new RootElement("smil"); 1767 Element body = root.getChild("body"); 1768 Element seq = body.getChild("seq"); 1769 Element media = seq.getChild("media"); 1770 media.setElementListener(this); 1771 1772 this.handler = root.getContentHandler(); 1773 } 1774 1775 @Override 1776 public void start(Attributes attributes) { 1777 String path = attributes.getValue("", "src"); 1778 if (path != null) { 1779 cachePlaylistEntry(path, playListDirectory); 1780 } 1781 } 1782 1783 @Override 1784 public void end() { 1785 } 1786 1787 ContentHandler getContentHandler() { 1788 return handler; 1789 } 1790 } 1791 1792 private void processWplPlayList(String path, String playListDirectory, Uri uri, 1793 ContentValues values, Cursor fileList) { 1794 FileInputStream fis = null; 1795 try { 1796 File f = new File(path); 1797 if (f.exists()) { 1798 fis = new FileInputStream(f); 1799 1800 mPlaylistEntries.clear(); 1801 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), 1802 new WplHandler(playListDirectory, uri, fileList).getContentHandler()); 1803 1804 processCachedPlaylist(fileList, values, uri); 1805 } 1806 } catch (SAXException e) { 1807 e.printStackTrace(); 1808 } catch (IOException e) { 1809 e.printStackTrace(); 1810 } finally { 1811 try { 1812 if (fis != null) 1813 fis.close(); 1814 } catch (IOException e) { 1815 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1816 } 1817 } 1818 } 1819 1820 private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException { 1821 String path = entry.mPath; 1822 ContentValues values = new ContentValues(); 1823 int lastSlash = path.lastIndexOf('/'); 1824 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1825 Uri uri, membersUri; 1826 long rowId = entry.mRowId; 1827 1828 // make sure we have a name 1829 String name = values.getAsString(MediaStore.Audio.Playlists.NAME); 1830 if (name == null) { 1831 name = values.getAsString(MediaStore.MediaColumns.TITLE); 1832 if (name == null) { 1833 // extract name from file name 1834 int lastDot = path.lastIndexOf('.'); 1835 name = (lastDot < 0 ? path.substring(lastSlash + 1) 1836 : path.substring(lastSlash + 1, lastDot)); 1837 } 1838 } 1839 1840 values.put(MediaStore.Audio.Playlists.NAME, name); 1841 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1842 1843 if (rowId == 0) { 1844 values.put(MediaStore.Audio.Playlists.DATA, path); 1845 uri = mMediaProvider.insert(mPlaylistsUri, values); 1846 rowId = ContentUris.parseId(uri); 1847 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1848 } else { 1849 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1850 mMediaProvider.update(uri, values, null, null); 1851 1852 // delete members of existing playlist 1853 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1854 mMediaProvider.delete(membersUri, null, null); 1855 } 1856 1857 String playListDirectory = path.substring(0, lastSlash + 1); 1858 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1859 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1860 1861 if (fileType == MediaFile.FILE_TYPE_M3U) { 1862 processM3uPlayList(path, playListDirectory, membersUri, values, fileList); 1863 } else if (fileType == MediaFile.FILE_TYPE_PLS) { 1864 processPlsPlayList(path, playListDirectory, membersUri, values, fileList); 1865 } else if (fileType == MediaFile.FILE_TYPE_WPL) { 1866 processWplPlayList(path, playListDirectory, membersUri, values, fileList); 1867 } 1868 } 1869 1870 private void processPlayLists() throws RemoteException { 1871 Iterator<FileEntry> iterator = mPlayLists.iterator(); 1872 Cursor fileList = null; 1873 try { 1874 // use the files uri and projection because we need the format column, 1875 // but restrict the query to just audio files 1876 fileList = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION, 1877 "media_type=2", null, null, null); 1878 while (iterator.hasNext()) { 1879 FileEntry entry = iterator.next(); 1880 // only process playlist files if they are new or have been modified since the last scan 1881 if (entry.mLastModifiedChanged) { 1882 processPlayList(entry, fileList); 1883 } 1884 } 1885 } catch (RemoteException e1) { 1886 } finally { 1887 if (fileList != null) { 1888 fileList.close(); 1889 } 1890 } 1891 } 1892 1893 private native void processDirectory(String path, MediaScannerClient client); 1894 private native void processFile(String path, String mimeType, MediaScannerClient client); 1895 private native void setLocale(String locale); 1896 1897 public native byte[] extractAlbumArt(FileDescriptor fd); 1898 1899 private static native final void native_init(); 1900 private native final void native_setup(); 1901 private native final void native_finalize(); 1902 1903 @Override 1904 public void close() { 1905 mCloseGuard.close(); 1906 if (mClosed.compareAndSet(false, true)) { 1907 mMediaProvider.close(); 1908 native_finalize(); 1909 } 1910 } 1911 1912 @Override 1913 protected void finalize() throws Throwable { 1914 try { 1915 mCloseGuard.warnIfOpen(); 1916 close(); 1917 } finally { 1918 super.finalize(); 1919 } 1920 } 1921 } 1922