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