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.content.SharedPreferences; 25 import android.database.Cursor; 26 import android.database.SQLException; 27 import android.drm.DrmManagerClient; 28 import android.graphics.BitmapFactory; 29 import android.mtp.MtpConstants; 30 import android.net.Uri; 31 import android.os.Build; 32 import android.os.Environment; 33 import android.os.RemoteException; 34 import android.os.SystemProperties; 35 import android.provider.MediaStore; 36 import android.provider.MediaStore.Audio; 37 import android.provider.MediaStore.Audio.Playlists; 38 import android.provider.MediaStore.Files; 39 import android.provider.MediaStore.Files.FileColumns; 40 import android.provider.MediaStore.Images; 41 import android.provider.MediaStore.Video; 42 import android.provider.Settings; 43 import android.provider.Settings.SettingNotFoundException; 44 import android.sax.Element; 45 import android.sax.ElementListener; 46 import android.sax.RootElement; 47 import android.system.ErrnoException; 48 import android.system.Os; 49 import android.text.TextUtils; 50 import android.util.Log; 51 import android.util.Xml; 52 53 import dalvik.system.CloseGuard; 54 55 import org.xml.sax.Attributes; 56 import org.xml.sax.ContentHandler; 57 import org.xml.sax.SAXException; 58 59 import java.io.BufferedReader; 60 import java.io.File; 61 import java.io.FileDescriptor; 62 import java.io.FileInputStream; 63 import java.io.IOException; 64 import java.io.InputStreamReader; 65 import java.text.SimpleDateFormat; 66 import java.text.ParseException; 67 import java.util.ArrayList; 68 import java.util.HashMap; 69 import java.util.HashSet; 70 import java.util.Iterator; 71 import java.util.Locale; 72 import java.util.TimeZone; 73 import 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 */ 120 public 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 String sLastInternalScanFingerprint; 162 163 private static final String[] ID3_GENRES = { 164 // ID3v1 Genres 165 "Blues", 166 "Classic Rock", 167 "Country", 168 "Dance", 169 "Disco", 170 "Funk", 171 "Grunge", 172 "Hip-Hop", 173 "Jazz", 174 "Metal", 175 "New Age", 176 "Oldies", 177 "Other", 178 "Pop", 179 "R&B", 180 "Rap", 181 "Reggae", 182 "Rock", 183 "Techno", 184 "Industrial", 185 "Alternative", 186 "Ska", 187 "Death Metal", 188 "Pranks", 189 "Soundtrack", 190 "Euro-Techno", 191 "Ambient", 192 "Trip-Hop", 193 "Vocal", 194 "Jazz+Funk", 195 "Fusion", 196 "Trance", 197 "Classical", 198 "Instrumental", 199 "Acid", 200 "House", 201 "Game", 202 "Sound Clip", 203 "Gospel", 204 "Noise", 205 "AlternRock", 206 "Bass", 207 "Soul", 208 "Punk", 209 "Space", 210 "Meditative", 211 "Instrumental Pop", 212 "Instrumental Rock", 213 "Ethnic", 214 "Gothic", 215 "Darkwave", 216 "Techno-Industrial", 217 "Electronic", 218 "Pop-Folk", 219 "Eurodance", 220 "Dream", 221 "Southern Rock", 222 "Comedy", 223 "Cult", 224 "Gangsta", 225 "Top 40", 226 "Christian Rap", 227 "Pop/Funk", 228 "Jungle", 229 "Native American", 230 "Cabaret", 231 "New Wave", 232 "Psychadelic", 233 "Rave", 234 "Showtunes", 235 "Trailer", 236 "Lo-Fi", 237 "Tribal", 238 "Acid Punk", 239 "Acid Jazz", 240 "Polka", 241 "Retro", 242 "Musical", 243 "Rock & Roll", 244 "Hard Rock", 245 // The following genres are Winamp extensions 246 "Folk", 247 "Folk-Rock", 248 "National Folk", 249 "Swing", 250 "Fast Fusion", 251 "Bebob", 252 "Latin", 253 "Revival", 254 "Celtic", 255 "Bluegrass", 256 "Avantgarde", 257 "Gothic Rock", 258 "Progressive Rock", 259 "Psychedelic Rock", 260 "Symphonic Rock", 261 "Slow Rock", 262 "Big Band", 263 "Chorus", 264 "Easy Listening", 265 "Acoustic", 266 "Humour", 267 "Speech", 268 "Chanson", 269 "Opera", 270 "Chamber Music", 271 "Sonata", 272 "Symphony", 273 "Booty Bass", 274 "Primus", 275 "Porn Groove", 276 "Satire", 277 "Slow Jam", 278 "Club", 279 "Tango", 280 "Samba", 281 "Folklore", 282 "Ballad", 283 "Power Ballad", 284 "Rhythmic Soul", 285 "Freestyle", 286 "Duet", 287 "Punk Rock", 288 "Drum Solo", 289 "A capella", 290 "Euro-House", 291 "Dance Hall", 292 // The following ones seem to be fairly widely supported as well 293 "Goa", 294 "Drum & Bass", 295 "Club-House", 296 "Hardcore", 297 "Terror", 298 "Indie", 299 "Britpop", 300 null, 301 "Polsk Punk", 302 "Beat", 303 "Christian Gangsta", 304 "Heavy Metal", 305 "Black Metal", 306 "Crossover", 307 "Contemporary Christian", 308 "Christian Rock", 309 "Merengue", 310 "Salsa", 311 "Thrash Metal", 312 "Anime", 313 "JPop", 314 "Synthpop", 315 // 148 and up don't seem to have been defined yet. 316 }; 317 318 private long mNativeContext; 319 private final Context mContext; 320 private final String mPackageName; 321 private final String mVolumeName; 322 private final ContentProviderClient mMediaProvider; 323 private final Uri mAudioUri; 324 private final Uri mVideoUri; 325 private final Uri mImagesUri; 326 private final Uri mThumbsUri; 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 mThumbsUri = Images.Thumbnails.getContentUri(volumeName); 423 mFilesUri = Files.getContentUri(volumeName); 424 mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build(); 425 426 if (!volumeName.equals("internal")) { 427 // we only support playlists on external media 428 mProcessPlaylists = true; 429 mProcessGenres = true; 430 mPlaylistsUri = Playlists.getContentUri(volumeName); 431 } else { 432 mProcessPlaylists = false; 433 mProcessGenres = false; 434 mPlaylistsUri = null; 435 } 436 437 final Locale locale = mContext.getResources().getConfiguration().locale; 438 if (locale != null) { 439 String language = locale.getLanguage(); 440 String country = locale.getCountry(); 441 if (language != null) { 442 if (country != null) { 443 setLocale(language + "_" + country); 444 } else { 445 setLocale(language); 446 } 447 } 448 } 449 450 mCloseGuard.open("close"); 451 } 452 453 private void setDefaultRingtoneFileNames() { 454 mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 455 + Settings.System.RINGTONE); 456 mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 457 + Settings.System.NOTIFICATION_SOUND); 458 mDefaultAlarmAlertFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 459 + Settings.System.ALARM_ALERT); 460 } 461 462 private final MyMediaScannerClient mClient = new MyMediaScannerClient(); 463 464 private boolean isDrmEnabled() { 465 String prop = SystemProperties.get("drm.service.enabled"); 466 return prop != null && prop.equals("true"); 467 } 468 469 private class MyMediaScannerClient implements MediaScannerClient { 470 471 private final SimpleDateFormat mDateFormatter; 472 473 private String mArtist; 474 private String mAlbumArtist; // use this if mArtist is missing 475 private String mAlbum; 476 private String mTitle; 477 private String mComposer; 478 private String mGenre; 479 private String mMimeType; 480 private int mFileType; 481 private int mTrack; 482 private int mYear; 483 private int mDuration; 484 private String mPath; 485 private long mDate; 486 private long mLastModified; 487 private long mFileSize; 488 private String mWriter; 489 private int mCompilation; 490 private boolean mIsDrm; 491 private boolean mNoMedia; // flag to suppress file from appearing in media tables 492 private int mWidth; 493 private int mHeight; 494 495 public MyMediaScannerClient() { 496 mDateFormatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss"); 497 mDateFormatter.setTimeZone(TimeZone.getTimeZone("UTC")); 498 } 499 500 public FileEntry beginFile(String path, String mimeType, long lastModified, 501 long fileSize, boolean isDirectory, boolean noMedia) { 502 mMimeType = mimeType; 503 mFileType = 0; 504 mFileSize = fileSize; 505 mIsDrm = false; 506 507 if (!isDirectory) { 508 if (!noMedia && isNoMediaFile(path)) { 509 noMedia = true; 510 } 511 mNoMedia = noMedia; 512 513 // try mimeType first, if it is specified 514 if (mimeType != null) { 515 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 516 } 517 518 // if mimeType was not specified, compute file type based on file extension. 519 if (mFileType == 0) { 520 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 521 if (mediaFileType != null) { 522 mFileType = mediaFileType.fileType; 523 if (mMimeType == null) { 524 mMimeType = mediaFileType.mimeType; 525 } 526 } 527 } 528 529 if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) { 530 mFileType = getFileTypeFromDrm(path); 531 } 532 } 533 534 FileEntry entry = makeEntryFor(path); 535 // add some slack to avoid a rounding error 536 long delta = (entry != null) ? (lastModified - entry.mLastModified) : 0; 537 boolean wasModified = delta > 1 || delta < -1; 538 if (entry == null || wasModified) { 539 if (wasModified) { 540 entry.mLastModified = lastModified; 541 } else { 542 entry = new FileEntry(0, path, lastModified, 543 (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0)); 544 } 545 entry.mLastModifiedChanged = true; 546 } 547 548 if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) { 549 mPlayLists.add(entry); 550 // we don't process playlists in the main scan, so return null 551 return null; 552 } 553 554 // clear all the metadata 555 mArtist = null; 556 mAlbumArtist = null; 557 mAlbum = null; 558 mTitle = null; 559 mComposer = null; 560 mGenre = null; 561 mTrack = 0; 562 mYear = 0; 563 mDuration = 0; 564 mPath = path; 565 mDate = 0; 566 mLastModified = lastModified; 567 mWriter = null; 568 mCompilation = 0; 569 mWidth = 0; 570 mHeight = 0; 571 572 return entry; 573 } 574 575 @Override 576 public void scanFile(String path, long lastModified, long fileSize, 577 boolean isDirectory, boolean noMedia) { 578 // This is the callback funtion from native codes. 579 // Log.v(TAG, "scanFile: "+path); 580 doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia); 581 } 582 583 public Uri doScanFile(String path, String mimeType, long lastModified, 584 long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) { 585 Uri result = null; 586 // long t1 = System.currentTimeMillis(); 587 try { 588 FileEntry entry = beginFile(path, mimeType, lastModified, 589 fileSize, isDirectory, noMedia); 590 591 if (entry == null) { 592 return null; 593 } 594 595 // if this file was just inserted via mtp, set the rowid to zero 596 // (even though it already exists in the database), to trigger 597 // the correct code path for updating its entry 598 if (mMtpObjectHandle != 0) { 599 entry.mRowId = 0; 600 } 601 602 if (entry.mPath != null) { 603 if (((!mDefaultNotificationSet && 604 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) 605 || (!mDefaultRingtoneSet && 606 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) 607 || (!mDefaultAlarmSet && 608 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)))) { 609 Log.w(TAG, "forcing rescan of " + entry.mPath + 610 "since ringtone setting didn't finish"); 611 scanAlways = true; 612 } else if (isSystemSoundWithMetadata(entry.mPath) 613 && !Build.FINGERPRINT.equals(sLastInternalScanFingerprint)) { 614 // file is located on the system partition where the date cannot be trusted: 615 // rescan if the build fingerprint has changed since the last scan. 616 Log.i(TAG, "forcing rescan of " + entry.mPath 617 + " since build fingerprint changed"); 618 scanAlways = true; 619 } 620 } 621 622 // rescan for metadata if file was modified since last scan 623 if (entry != null && (entry.mLastModifiedChanged || scanAlways)) { 624 if (noMedia) { 625 result = endFile(entry, false, false, false, false, false); 626 } else { 627 String lowpath = path.toLowerCase(Locale.ROOT); 628 boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0); 629 boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0); 630 boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0); 631 boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0); 632 boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) || 633 (!ringtones && !notifications && !alarms && !podcasts); 634 635 boolean isaudio = MediaFile.isAudioFileType(mFileType); 636 boolean isvideo = MediaFile.isVideoFileType(mFileType); 637 boolean isimage = MediaFile.isImageFileType(mFileType); 638 639 if (isaudio || isvideo || isimage) { 640 path = Environment.maybeTranslateEmulatedPathToInternal(new File(path)) 641 .getAbsolutePath(); 642 } 643 644 // we only extract metadata for audio and video files 645 if (isaudio || isvideo) { 646 processFile(path, mimeType, this); 647 } 648 649 if (isimage) { 650 processImageFile(path); 651 } 652 653 result = endFile(entry, ringtones, notifications, alarms, music, podcasts); 654 } 655 } 656 } catch (RemoteException e) { 657 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 658 } 659 // long t2 = System.currentTimeMillis(); 660 // Log.v(TAG, "scanFile: " + path + " took " + (t2-t1)); 661 return result; 662 } 663 664 private long parseDate(String date) { 665 try { 666 return mDateFormatter.parse(date).getTime(); 667 } catch (ParseException e) { 668 return 0; 669 } 670 } 671 672 private int parseSubstring(String s, int start, int defaultValue) { 673 int length = s.length(); 674 if (start == length) return defaultValue; 675 676 char ch = s.charAt(start++); 677 // return defaultValue if we have no integer at all 678 if (ch < '0' || ch > '9') return defaultValue; 679 680 int result = ch - '0'; 681 while (start < length) { 682 ch = s.charAt(start++); 683 if (ch < '0' || ch > '9') return result; 684 result = result * 10 + (ch - '0'); 685 } 686 687 return result; 688 } 689 690 public void handleStringTag(String name, String value) { 691 if (name.equalsIgnoreCase("title") || name.startsWith("title;")) { 692 // Don't trim() here, to preserve the special \001 character 693 // used to force sorting. The media provider will trim() before 694 // inserting the title in to the database. 695 mTitle = value; 696 } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) { 697 mArtist = value.trim(); 698 } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;") 699 || name.equalsIgnoreCase("band") || name.startsWith("band;")) { 700 mAlbumArtist = value.trim(); 701 } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) { 702 mAlbum = value.trim(); 703 } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) { 704 mComposer = value.trim(); 705 } else if (mProcessGenres && 706 (name.equalsIgnoreCase("genre") || name.startsWith("genre;"))) { 707 mGenre = getGenreName(value); 708 } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) { 709 mYear = parseSubstring(value, 0, 0); 710 } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) { 711 // track number might be of the form "2/12" 712 // we just read the number before the slash 713 int num = parseSubstring(value, 0, 0); 714 mTrack = (mTrack / 1000) * 1000 + num; 715 } else if (name.equalsIgnoreCase("discnumber") || 716 name.equals("set") || name.startsWith("set;")) { 717 // set number might be of the form "1/3" 718 // we just read the number before the slash 719 int num = parseSubstring(value, 0, 0); 720 mTrack = (num * 1000) + (mTrack % 1000); 721 } else if (name.equalsIgnoreCase("duration")) { 722 mDuration = parseSubstring(value, 0, 0); 723 } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) { 724 mWriter = value.trim(); 725 } else if (name.equalsIgnoreCase("compilation")) { 726 mCompilation = parseSubstring(value, 0, 0); 727 } else if (name.equalsIgnoreCase("isdrm")) { 728 mIsDrm = (parseSubstring(value, 0, 0) == 1); 729 } else if (name.equalsIgnoreCase("date")) { 730 mDate = parseDate(value); 731 } else if (name.equalsIgnoreCase("width")) { 732 mWidth = parseSubstring(value, 0, 0); 733 } else if (name.equalsIgnoreCase("height")) { 734 mHeight = parseSubstring(value, 0, 0); 735 } else { 736 //Log.v(TAG, "unknown tag: " + name + " (" + mProcessGenres + ")"); 737 } 738 } 739 740 private boolean convertGenreCode(String input, String expected) { 741 String output = getGenreName(input); 742 if (output.equals(expected)) { 743 return true; 744 } else { 745 Log.d(TAG, "'" + input + "' -> '" + output + "', expected '" + expected + "'"); 746 return false; 747 } 748 } 749 private void testGenreNameConverter() { 750 convertGenreCode("2", "Country"); 751 convertGenreCode("(2)", "Country"); 752 convertGenreCode("(2", "(2"); 753 convertGenreCode("2 Foo", "Country"); 754 convertGenreCode("(2) Foo", "Country"); 755 convertGenreCode("(2 Foo", "(2 Foo"); 756 convertGenreCode("2Foo", "2Foo"); 757 convertGenreCode("(2)Foo", "Country"); 758 convertGenreCode("200 Foo", "Foo"); 759 convertGenreCode("(200) Foo", "Foo"); 760 convertGenreCode("200Foo", "200Foo"); 761 convertGenreCode("(200)Foo", "Foo"); 762 convertGenreCode("200)Foo", "200)Foo"); 763 convertGenreCode("200) Foo", "200) Foo"); 764 } 765 766 public String getGenreName(String genreTagValue) { 767 768 if (genreTagValue == null) { 769 return null; 770 } 771 final int length = genreTagValue.length(); 772 773 if (length > 0) { 774 boolean parenthesized = false; 775 StringBuffer number = new StringBuffer(); 776 int i = 0; 777 for (; i < length; ++i) { 778 char c = genreTagValue.charAt(i); 779 if (i == 0 && c == '(') { 780 parenthesized = true; 781 } else if (Character.isDigit(c)) { 782 number.append(c); 783 } else { 784 break; 785 } 786 } 787 char charAfterNumber = i < length ? genreTagValue.charAt(i) : ' '; 788 if ((parenthesized && charAfterNumber == ')') 789 || !parenthesized && Character.isWhitespace(charAfterNumber)) { 790 try { 791 short genreIndex = Short.parseShort(number.toString()); 792 if (genreIndex >= 0) { 793 if (genreIndex < ID3_GENRES.length && ID3_GENRES[genreIndex] != null) { 794 return ID3_GENRES[genreIndex]; 795 } else if (genreIndex == 0xFF) { 796 return null; 797 } else if (genreIndex < 0xFF && (i + 1) < length) { 798 // genre is valid but unknown, 799 // if there is a string after the value we take it 800 if (parenthesized && charAfterNumber == ')') { 801 i++; 802 } 803 String ret = genreTagValue.substring(i).trim(); 804 if (ret.length() != 0) { 805 return ret; 806 } 807 } else { 808 // else return the number, without parentheses 809 return number.toString(); 810 } 811 } 812 } catch (NumberFormatException e) { 813 } 814 } 815 } 816 817 return genreTagValue; 818 } 819 820 private void processImageFile(String path) { 821 try { 822 mBitmapOptions.outWidth = 0; 823 mBitmapOptions.outHeight = 0; 824 BitmapFactory.decodeFile(path, mBitmapOptions); 825 mWidth = mBitmapOptions.outWidth; 826 mHeight = mBitmapOptions.outHeight; 827 } catch (Throwable th) { 828 // ignore; 829 } 830 } 831 832 public void setMimeType(String mimeType) { 833 if ("audio/mp4".equals(mMimeType) && 834 mimeType.startsWith("video")) { 835 // for feature parity with Donut, we force m4a files to keep the 836 // audio/mp4 mimetype, even if they are really "enhanced podcasts" 837 // with a video track 838 return; 839 } 840 mMimeType = mimeType; 841 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 842 } 843 844 /** 845 * Formats the data into a values array suitable for use with the Media 846 * Content Provider. 847 * 848 * @return a map of values 849 */ 850 private ContentValues toValues() { 851 ContentValues map = new ContentValues(); 852 853 map.put(MediaStore.MediaColumns.DATA, mPath); 854 map.put(MediaStore.MediaColumns.TITLE, mTitle); 855 map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified); 856 map.put(MediaStore.MediaColumns.SIZE, mFileSize); 857 map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType); 858 map.put(MediaStore.MediaColumns.IS_DRM, mIsDrm); 859 860 String resolution = null; 861 if (mWidth > 0 && mHeight > 0) { 862 map.put(MediaStore.MediaColumns.WIDTH, mWidth); 863 map.put(MediaStore.MediaColumns.HEIGHT, mHeight); 864 resolution = mWidth + "x" + mHeight; 865 } 866 867 if (!mNoMedia) { 868 if (MediaFile.isVideoFileType(mFileType)) { 869 map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 870 ? mArtist : MediaStore.UNKNOWN_STRING)); 871 map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 872 ? mAlbum : MediaStore.UNKNOWN_STRING)); 873 map.put(Video.Media.DURATION, mDuration); 874 if (resolution != null) { 875 map.put(Video.Media.RESOLUTION, resolution); 876 } 877 if (mDate > 0) { 878 map.put(Video.Media.DATE_TAKEN, mDate); 879 } 880 } else if (MediaFile.isImageFileType(mFileType)) { 881 // FIXME - add DESCRIPTION 882 } else if (MediaFile.isAudioFileType(mFileType)) { 883 map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ? 884 mArtist : MediaStore.UNKNOWN_STRING); 885 map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null && 886 mAlbumArtist.length() > 0) ? mAlbumArtist : null); 887 map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ? 888 mAlbum : MediaStore.UNKNOWN_STRING); 889 map.put(Audio.Media.COMPOSER, mComposer); 890 map.put(Audio.Media.GENRE, mGenre); 891 if (mYear != 0) { 892 map.put(Audio.Media.YEAR, mYear); 893 } 894 map.put(Audio.Media.TRACK, mTrack); 895 map.put(Audio.Media.DURATION, mDuration); 896 map.put(Audio.Media.COMPILATION, mCompilation); 897 } 898 } 899 return map; 900 } 901 902 private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications, 903 boolean alarms, boolean music, boolean podcasts) 904 throws RemoteException { 905 // update database 906 907 // use album artist if artist is missing 908 if (mArtist == null || mArtist.length() == 0) { 909 mArtist = mAlbumArtist; 910 } 911 912 ContentValues values = toValues(); 913 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 914 if (title == null || TextUtils.isEmpty(title.trim())) { 915 title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA)); 916 values.put(MediaStore.MediaColumns.TITLE, title); 917 } 918 String album = values.getAsString(Audio.Media.ALBUM); 919 if (MediaStore.UNKNOWN_STRING.equals(album)) { 920 album = values.getAsString(MediaStore.MediaColumns.DATA); 921 // extract last path segment before file name 922 int lastSlash = album.lastIndexOf('/'); 923 if (lastSlash >= 0) { 924 int previousSlash = 0; 925 while (true) { 926 int idx = album.indexOf('/', previousSlash + 1); 927 if (idx < 0 || idx >= lastSlash) { 928 break; 929 } 930 previousSlash = idx; 931 } 932 if (previousSlash != 0) { 933 album = album.substring(previousSlash + 1, lastSlash); 934 values.put(Audio.Media.ALBUM, album); 935 } 936 } 937 } 938 long rowId = entry.mRowId; 939 if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) { 940 // Only set these for new entries. For existing entries, they 941 // may have been modified later, and we want to keep the current 942 // values so that custom ringtones still show up in the ringtone 943 // picker. 944 values.put(Audio.Media.IS_RINGTONE, ringtones); 945 values.put(Audio.Media.IS_NOTIFICATION, notifications); 946 values.put(Audio.Media.IS_ALARM, alarms); 947 values.put(Audio.Media.IS_MUSIC, music); 948 values.put(Audio.Media.IS_PODCAST, podcasts); 949 } else if ((mFileType == MediaFile.FILE_TYPE_JPEG 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 return true; 1158 } 1159 return false; 1160 } 1161 1162 private String settingSetIndicatorName(String base) { 1163 return base + "_set"; 1164 } 1165 1166 private boolean wasRingtoneAlreadySet(String name) { 1167 ContentResolver cr = mContext.getContentResolver(); 1168 String indicatorName = settingSetIndicatorName(name); 1169 try { 1170 return Settings.System.getInt(cr, indicatorName) != 0; 1171 } catch (SettingNotFoundException e) { 1172 return false; 1173 } 1174 } 1175 1176 private void prescan(String filePath, boolean prescanFiles) throws RemoteException { 1177 Cursor c = null; 1178 String where = null; 1179 String[] selectionArgs = null; 1180 1181 mPlayLists.clear(); 1182 1183 if (filePath != null) { 1184 // query for only one file 1185 where = MediaStore.Files.FileColumns._ID + ">?" + 1186 " AND " + Files.FileColumns.DATA + "=?"; 1187 selectionArgs = new String[] { "", filePath }; 1188 } else { 1189 where = MediaStore.Files.FileColumns._ID + ">?"; 1190 selectionArgs = new String[] { "" }; 1191 } 1192 1193 mDefaultRingtoneSet = wasRingtoneAlreadySet(Settings.System.RINGTONE); 1194 mDefaultNotificationSet = wasRingtoneAlreadySet(Settings.System.NOTIFICATION_SOUND); 1195 mDefaultAlarmSet = wasRingtoneAlreadySet(Settings.System.ALARM_ALERT); 1196 1197 // Tell the provider to not delete the file. 1198 // If the file is truly gone the delete is unnecessary, and we want to avoid 1199 // accidentally deleting files that are really there (this may happen if the 1200 // filesystem is mounted and unmounted while the scanner is running). 1201 Uri.Builder builder = mFilesUri.buildUpon(); 1202 builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false"); 1203 MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, builder.build()); 1204 1205 // Build the list of files from the content provider 1206 try { 1207 if (prescanFiles) { 1208 // First read existing files from the files table. 1209 // Because we'll be deleting entries for missing files as we go, 1210 // we need to query the database in small batches, to avoid problems 1211 // with CursorWindow positioning. 1212 long lastId = Long.MIN_VALUE; 1213 Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build(); 1214 1215 while (true) { 1216 selectionArgs[0] = "" + lastId; 1217 if (c != null) { 1218 c.close(); 1219 c = null; 1220 } 1221 c = mMediaProvider.query(limitUri, FILES_PRESCAN_PROJECTION, 1222 where, selectionArgs, MediaStore.Files.FileColumns._ID, null); 1223 if (c == null) { 1224 break; 1225 } 1226 1227 int num = c.getCount(); 1228 1229 if (num == 0) { 1230 break; 1231 } 1232 while (c.moveToNext()) { 1233 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1234 String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); 1235 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1236 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1237 lastId = rowId; 1238 1239 // Only consider entries with absolute path names. 1240 // This allows storing URIs in the database without the 1241 // media scanner removing them. 1242 if (path != null && path.startsWith("/")) { 1243 boolean exists = false; 1244 try { 1245 exists = Os.access(path, android.system.OsConstants.F_OK); 1246 } catch (ErrnoException e1) { 1247 } 1248 if (!exists && !MtpConstants.isAbstractObject(format)) { 1249 // do not delete missing playlists, since they may have been 1250 // modified by the user. 1251 // The user can delete them in the media player instead. 1252 // instead, clear the path and lastModified fields in the row 1253 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1254 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1255 1256 if (!MediaFile.isPlayListFileType(fileType)) { 1257 deleter.delete(rowId); 1258 if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) { 1259 deleter.flush(); 1260 String parent = new File(path).getParent(); 1261 mMediaProvider.call(MediaStore.UNHIDE_CALL, parent, null); 1262 } 1263 } 1264 } 1265 } 1266 } 1267 } 1268 } 1269 } 1270 finally { 1271 if (c != null) { 1272 c.close(); 1273 } 1274 deleter.flush(); 1275 } 1276 1277 // compute original size of images 1278 mOriginalCount = 0; 1279 c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null, null); 1280 if (c != null) { 1281 mOriginalCount = c.getCount(); 1282 c.close(); 1283 } 1284 } 1285 1286 private void pruneDeadThumbnailFiles() { 1287 HashSet<String> existingFiles = new HashSet<String>(); 1288 String directory = "/sdcard/DCIM/.thumbnails"; 1289 String [] files = (new File(directory)).list(); 1290 Cursor c = null; 1291 if (files == null) 1292 files = new String[0]; 1293 1294 for (int i = 0; i < files.length; i++) { 1295 String fullPathString = directory + "/" + files[i]; 1296 existingFiles.add(fullPathString); 1297 } 1298 1299 try { 1300 c = mMediaProvider.query( 1301 mThumbsUri, 1302 new String [] { "_data" }, 1303 null, 1304 null, 1305 null, null); 1306 Log.v(TAG, "pruneDeadThumbnailFiles... " + c); 1307 if (c != null && c.moveToFirst()) { 1308 do { 1309 String fullPathString = c.getString(0); 1310 existingFiles.remove(fullPathString); 1311 } while (c.moveToNext()); 1312 } 1313 1314 for (String fileToDelete : existingFiles) { 1315 if (false) 1316 Log.v(TAG, "fileToDelete is " + fileToDelete); 1317 try { 1318 (new File(fileToDelete)).delete(); 1319 } catch (SecurityException ex) { 1320 } 1321 } 1322 1323 Log.v(TAG, "/pruneDeadThumbnailFiles... " + c); 1324 } catch (RemoteException e) { 1325 // We will soon be killed... 1326 } finally { 1327 if (c != null) { 1328 c.close(); 1329 } 1330 } 1331 } 1332 1333 static class MediaBulkDeleter { 1334 StringBuilder whereClause = new StringBuilder(); 1335 ArrayList<String> whereArgs = new ArrayList<String>(100); 1336 final ContentProviderClient mProvider; 1337 final Uri mBaseUri; 1338 1339 public MediaBulkDeleter(ContentProviderClient provider, Uri baseUri) { 1340 mProvider = provider; 1341 mBaseUri = baseUri; 1342 } 1343 1344 public void delete(long id) throws RemoteException { 1345 if (whereClause.length() != 0) { 1346 whereClause.append(","); 1347 } 1348 whereClause.append("?"); 1349 whereArgs.add("" + id); 1350 if (whereArgs.size() > 100) { 1351 flush(); 1352 } 1353 } 1354 public void flush() throws RemoteException { 1355 int size = whereArgs.size(); 1356 if (size > 0) { 1357 String [] foo = new String [size]; 1358 foo = whereArgs.toArray(foo); 1359 int numrows = mProvider.delete(mBaseUri, 1360 MediaStore.MediaColumns._ID + " IN (" + 1361 whereClause.toString() + ")", foo); 1362 //Log.i("@@@@@@@@@", "rows deleted: " + numrows); 1363 whereClause.setLength(0); 1364 whereArgs.clear(); 1365 } 1366 } 1367 } 1368 1369 private void postscan(final String[] directories) throws RemoteException { 1370 1371 // handle playlists last, after we know what media files are on the storage. 1372 if (mProcessPlaylists) { 1373 processPlayLists(); 1374 } 1375 1376 if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external"))) 1377 pruneDeadThumbnailFiles(); 1378 1379 // allow GC to clean up 1380 mPlayLists.clear(); 1381 } 1382 1383 private void releaseResources() { 1384 // release the DrmManagerClient resources 1385 if (mDrmManagerClient != null) { 1386 mDrmManagerClient.close(); 1387 mDrmManagerClient = null; 1388 } 1389 } 1390 1391 public void scanDirectories(String[] directories) { 1392 try { 1393 long start = System.currentTimeMillis(); 1394 prescan(null, true); 1395 long prescan = System.currentTimeMillis(); 1396 1397 if (ENABLE_BULK_INSERTS) { 1398 // create MediaInserter for bulk inserts 1399 mMediaInserter = new MediaInserter(mMediaProvider, 500); 1400 } 1401 1402 for (int i = 0; i < directories.length; i++) { 1403 processDirectory(directories[i], mClient); 1404 } 1405 1406 if (ENABLE_BULK_INSERTS) { 1407 // flush remaining inserts 1408 mMediaInserter.flushAll(); 1409 mMediaInserter = null; 1410 } 1411 1412 long scan = System.currentTimeMillis(); 1413 postscan(directories); 1414 long end = System.currentTimeMillis(); 1415 1416 if (false) { 1417 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1418 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1419 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1420 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1421 } 1422 } catch (SQLException e) { 1423 // this might happen if the SD card is removed while the media scanner is running 1424 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1425 } catch (UnsupportedOperationException e) { 1426 // this might happen if the SD card is removed while the media scanner is running 1427 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1428 } catch (RemoteException e) { 1429 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1430 } finally { 1431 releaseResources(); 1432 } 1433 } 1434 1435 // this function is used to scan a single file 1436 public Uri scanSingleFile(String path, String mimeType) { 1437 try { 1438 prescan(path, true); 1439 1440 File file = new File(path); 1441 if (!file.exists() || !file.canRead()) { 1442 return null; 1443 } 1444 1445 // lastModified is in milliseconds on Files. 1446 long lastModifiedSeconds = file.lastModified() / 1000; 1447 1448 // always scan the file, so we can return the content://media Uri for existing files 1449 return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(), 1450 false, true, MediaScanner.isNoMediaPath(path)); 1451 } catch (RemoteException e) { 1452 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1453 return null; 1454 } finally { 1455 releaseResources(); 1456 } 1457 } 1458 1459 private static boolean isNoMediaFile(String path) { 1460 File file = new File(path); 1461 if (file.isDirectory()) return false; 1462 1463 // special case certain file names 1464 // I use regionMatches() instead of substring() below 1465 // to avoid memory allocation 1466 int lastSlash = path.lastIndexOf('/'); 1467 if (lastSlash >= 0 && lastSlash + 2 < path.length()) { 1468 // ignore those ._* files created by MacOS 1469 if (path.regionMatches(lastSlash + 1, "._", 0, 2)) { 1470 return true; 1471 } 1472 1473 // ignore album art files created by Windows Media Player: 1474 // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg 1475 // and AlbumArt_{...}_Small.jpg 1476 if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) { 1477 if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) || 1478 path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) { 1479 return true; 1480 } 1481 int length = path.length() - lastSlash - 1; 1482 if ((length == 17 && path.regionMatches( 1483 true, lastSlash + 1, "AlbumArtSmall", 0, 13)) || 1484 (length == 10 1485 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) { 1486 return true; 1487 } 1488 } 1489 } 1490 return false; 1491 } 1492 1493 private static HashMap<String,String> mNoMediaPaths = new HashMap<String,String>(); 1494 private static HashMap<String,String> mMediaPaths = new HashMap<String,String>(); 1495 1496 /* MediaProvider calls this when a .nomedia file is added or removed */ 1497 public static void clearMediaPathCache(boolean clearMediaPaths, boolean clearNoMediaPaths) { 1498 synchronized (MediaScanner.class) { 1499 if (clearMediaPaths) { 1500 mMediaPaths.clear(); 1501 } 1502 if (clearNoMediaPaths) { 1503 mNoMediaPaths.clear(); 1504 } 1505 } 1506 } 1507 1508 public static boolean isNoMediaPath(String path) { 1509 if (path == null) { 1510 return false; 1511 } 1512 // return true if file or any parent directory has name starting with a dot 1513 if (path.indexOf("/.") >= 0) { 1514 return true; 1515 } 1516 1517 int firstSlash = path.lastIndexOf('/'); 1518 if (firstSlash <= 0) { 1519 return false; 1520 } 1521 String parent = path.substring(0, firstSlash); 1522 1523 synchronized (MediaScanner.class) { 1524 if (mNoMediaPaths.containsKey(parent)) { 1525 return true; 1526 } else if (!mMediaPaths.containsKey(parent)) { 1527 // check to see if any parent directories have a ".nomedia" file 1528 // start from 1 so we don't bother checking in the root directory 1529 int offset = 1; 1530 while (offset >= 0) { 1531 int slashIndex = path.indexOf('/', offset); 1532 if (slashIndex > offset) { 1533 slashIndex++; // move past slash 1534 File file = new File(path.substring(0, slashIndex) + ".nomedia"); 1535 if (file.exists()) { 1536 // we have a .nomedia in one of the parent directories 1537 mNoMediaPaths.put(parent, ""); 1538 return true; 1539 } 1540 } 1541 offset = slashIndex; 1542 } 1543 mMediaPaths.put(parent, ""); 1544 } 1545 } 1546 1547 return isNoMediaFile(path); 1548 } 1549 1550 public void scanMtpFile(String path, int objectHandle, int format) { 1551 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1552 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1553 File file = new File(path); 1554 long lastModifiedSeconds = file.lastModified() / 1000; 1555 1556 if (!MediaFile.isAudioFileType(fileType) && !MediaFile.isVideoFileType(fileType) && 1557 !MediaFile.isImageFileType(fileType) && !MediaFile.isPlayListFileType(fileType) && 1558 !MediaFile.isDrmFileType(fileType)) { 1559 1560 // no need to use the media scanner, but we need to update last modified and file size 1561 ContentValues values = new ContentValues(); 1562 values.put(Files.FileColumns.SIZE, file.length()); 1563 values.put(Files.FileColumns.DATE_MODIFIED, lastModifiedSeconds); 1564 try { 1565 String[] whereArgs = new String[] { Integer.toString(objectHandle) }; 1566 mMediaProvider.update(Files.getMtpObjectsUri(mVolumeName), values, 1567 "_id=?", whereArgs); 1568 } catch (RemoteException e) { 1569 Log.e(TAG, "RemoteException in scanMtpFile", e); 1570 } 1571 return; 1572 } 1573 1574 mMtpObjectHandle = objectHandle; 1575 Cursor fileList = null; 1576 try { 1577 if (MediaFile.isPlayListFileType(fileType)) { 1578 // build file cache so we can look up tracks in the playlist 1579 prescan(null, true); 1580 1581 FileEntry entry = makeEntryFor(path); 1582 if (entry != null) { 1583 fileList = mMediaProvider.query(mFilesUri, 1584 FILES_PRESCAN_PROJECTION, null, null, null, null); 1585 processPlayList(entry, fileList); 1586 } 1587 } else { 1588 // MTP will create a file entry for us so we don't want to do it in prescan 1589 prescan(path, false); 1590 1591 // always scan the file, so we can return the content://media Uri for existing files 1592 mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(), 1593 (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path)); 1594 } 1595 } catch (RemoteException e) { 1596 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1597 } finally { 1598 mMtpObjectHandle = 0; 1599 if (fileList != null) { 1600 fileList.close(); 1601 } 1602 releaseResources(); 1603 } 1604 } 1605 1606 FileEntry makeEntryFor(String path) { 1607 String where; 1608 String[] selectionArgs; 1609 1610 Cursor c = null; 1611 try { 1612 where = Files.FileColumns.DATA + "=?"; 1613 selectionArgs = new String[] { path }; 1614 c = mMediaProvider.query(mFilesUriNoNotify, FILES_PRESCAN_PROJECTION, 1615 where, selectionArgs, null, null); 1616 if (c.moveToFirst()) { 1617 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1618 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1619 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1620 return new FileEntry(rowId, path, lastModified, format); 1621 } 1622 } catch (RemoteException e) { 1623 } finally { 1624 if (c != null) { 1625 c.close(); 1626 } 1627 } 1628 return null; 1629 } 1630 1631 // returns the number of matching file/directory names, starting from the right 1632 private int matchPaths(String path1, String path2) { 1633 int result = 0; 1634 int end1 = path1.length(); 1635 int end2 = path2.length(); 1636 1637 while (end1 > 0 && end2 > 0) { 1638 int slash1 = path1.lastIndexOf('/', end1 - 1); 1639 int slash2 = path2.lastIndexOf('/', end2 - 1); 1640 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1641 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1642 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1643 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1644 if (start1 < 0) start1 = 0; else start1++; 1645 if (start2 < 0) start2 = 0; else start2++; 1646 int length = end1 - start1; 1647 if (end2 - start2 != length) break; 1648 if (path1.regionMatches(true, start1, path2, start2, length)) { 1649 result++; 1650 end1 = start1 - 1; 1651 end2 = start2 - 1; 1652 } else break; 1653 } 1654 1655 return result; 1656 } 1657 1658 private boolean matchEntries(long rowId, String data) { 1659 1660 int len = mPlaylistEntries.size(); 1661 boolean done = true; 1662 for (int i = 0; i < len; i++) { 1663 PlaylistEntry entry = mPlaylistEntries.get(i); 1664 if (entry.bestmatchlevel == Integer.MAX_VALUE) { 1665 continue; // this entry has been matched already 1666 } 1667 done = false; 1668 if (data.equalsIgnoreCase(entry.path)) { 1669 entry.bestmatchid = rowId; 1670 entry.bestmatchlevel = Integer.MAX_VALUE; 1671 continue; // no need for path matching 1672 } 1673 1674 int matchLength = matchPaths(data, entry.path); 1675 if (matchLength > entry.bestmatchlevel) { 1676 entry.bestmatchid = rowId; 1677 entry.bestmatchlevel = matchLength; 1678 } 1679 } 1680 return done; 1681 } 1682 1683 private void cachePlaylistEntry(String line, String playListDirectory) { 1684 PlaylistEntry entry = new PlaylistEntry(); 1685 // watch for trailing whitespace 1686 int entryLength = line.length(); 1687 while (entryLength > 0 && Character.isWhitespace(line.charAt(entryLength - 1))) entryLength--; 1688 // path should be longer than 3 characters. 1689 // avoid index out of bounds errors below by returning here. 1690 if (entryLength < 3) return; 1691 if (entryLength < line.length()) line = line.substring(0, entryLength); 1692 1693 // does entry appear to be an absolute path? 1694 // look for Unix or DOS absolute paths 1695 char ch1 = line.charAt(0); 1696 boolean fullPath = (ch1 == '/' || 1697 (Character.isLetter(ch1) && line.charAt(1) == ':' && line.charAt(2) == '\\')); 1698 // if we have a relative path, combine entry with playListDirectory 1699 if (!fullPath) 1700 line = playListDirectory + line; 1701 entry.path = line; 1702 //FIXME - should we look for "../" within the path? 1703 1704 mPlaylistEntries.add(entry); 1705 } 1706 1707 private void processCachedPlaylist(Cursor fileList, ContentValues values, Uri playlistUri) { 1708 fileList.moveToPosition(-1); 1709 while (fileList.moveToNext()) { 1710 long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1711 String data = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); 1712 if (matchEntries(rowId, data)) { 1713 break; 1714 } 1715 } 1716 1717 int len = mPlaylistEntries.size(); 1718 int index = 0; 1719 for (int i = 0; i < len; i++) { 1720 PlaylistEntry entry = mPlaylistEntries.get(i); 1721 if (entry.bestmatchlevel > 0) { 1722 try { 1723 values.clear(); 1724 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1725 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(entry.bestmatchid)); 1726 mMediaProvider.insert(playlistUri, values); 1727 index++; 1728 } catch (RemoteException e) { 1729 Log.e(TAG, "RemoteException in MediaScanner.processCachedPlaylist()", e); 1730 return; 1731 } 1732 } 1733 } 1734 mPlaylistEntries.clear(); 1735 } 1736 1737 private void processM3uPlayList(String path, String playListDirectory, Uri uri, 1738 ContentValues values, Cursor fileList) { 1739 BufferedReader reader = null; 1740 try { 1741 File f = new File(path); 1742 if (f.exists()) { 1743 reader = new BufferedReader( 1744 new InputStreamReader(new FileInputStream(f)), 8192); 1745 String line = reader.readLine(); 1746 mPlaylistEntries.clear(); 1747 while (line != null) { 1748 // ignore comment lines, which begin with '#' 1749 if (line.length() > 0 && line.charAt(0) != '#') { 1750 cachePlaylistEntry(line, playListDirectory); 1751 } 1752 line = reader.readLine(); 1753 } 1754 1755 processCachedPlaylist(fileList, values, uri); 1756 } 1757 } catch (IOException e) { 1758 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1759 } finally { 1760 try { 1761 if (reader != null) 1762 reader.close(); 1763 } catch (IOException e) { 1764 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1765 } 1766 } 1767 } 1768 1769 private void processPlsPlayList(String path, String playListDirectory, Uri uri, 1770 ContentValues values, Cursor fileList) { 1771 BufferedReader reader = null; 1772 try { 1773 File f = new File(path); 1774 if (f.exists()) { 1775 reader = new BufferedReader( 1776 new InputStreamReader(new FileInputStream(f)), 8192); 1777 String line = reader.readLine(); 1778 mPlaylistEntries.clear(); 1779 while (line != null) { 1780 // ignore comment lines, which begin with '#' 1781 if (line.startsWith("File")) { 1782 int equals = line.indexOf('='); 1783 if (equals > 0) { 1784 cachePlaylistEntry(line.substring(equals + 1), playListDirectory); 1785 } 1786 } 1787 line = reader.readLine(); 1788 } 1789 1790 processCachedPlaylist(fileList, values, uri); 1791 } 1792 } catch (IOException e) { 1793 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1794 } finally { 1795 try { 1796 if (reader != null) 1797 reader.close(); 1798 } catch (IOException e) { 1799 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1800 } 1801 } 1802 } 1803 1804 class WplHandler implements ElementListener { 1805 1806 final ContentHandler handler; 1807 String playListDirectory; 1808 1809 public WplHandler(String playListDirectory, Uri uri, Cursor fileList) { 1810 this.playListDirectory = playListDirectory; 1811 1812 RootElement root = new RootElement("smil"); 1813 Element body = root.getChild("body"); 1814 Element seq = body.getChild("seq"); 1815 Element media = seq.getChild("media"); 1816 media.setElementListener(this); 1817 1818 this.handler = root.getContentHandler(); 1819 } 1820 1821 @Override 1822 public void start(Attributes attributes) { 1823 String path = attributes.getValue("", "src"); 1824 if (path != null) { 1825 cachePlaylistEntry(path, playListDirectory); 1826 } 1827 } 1828 1829 @Override 1830 public void end() { 1831 } 1832 1833 ContentHandler getContentHandler() { 1834 return handler; 1835 } 1836 } 1837 1838 private void processWplPlayList(String path, String playListDirectory, Uri uri, 1839 ContentValues values, Cursor fileList) { 1840 FileInputStream fis = null; 1841 try { 1842 File f = new File(path); 1843 if (f.exists()) { 1844 fis = new FileInputStream(f); 1845 1846 mPlaylistEntries.clear(); 1847 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), 1848 new WplHandler(playListDirectory, uri, fileList).getContentHandler()); 1849 1850 processCachedPlaylist(fileList, values, uri); 1851 } 1852 } catch (SAXException e) { 1853 e.printStackTrace(); 1854 } catch (IOException e) { 1855 e.printStackTrace(); 1856 } finally { 1857 try { 1858 if (fis != null) 1859 fis.close(); 1860 } catch (IOException e) { 1861 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1862 } 1863 } 1864 } 1865 1866 private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException { 1867 String path = entry.mPath; 1868 ContentValues values = new ContentValues(); 1869 int lastSlash = path.lastIndexOf('/'); 1870 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1871 Uri uri, membersUri; 1872 long rowId = entry.mRowId; 1873 1874 // make sure we have a name 1875 String name = values.getAsString(MediaStore.Audio.Playlists.NAME); 1876 if (name == null) { 1877 name = values.getAsString(MediaStore.MediaColumns.TITLE); 1878 if (name == null) { 1879 // extract name from file name 1880 int lastDot = path.lastIndexOf('.'); 1881 name = (lastDot < 0 ? path.substring(lastSlash + 1) 1882 : path.substring(lastSlash + 1, lastDot)); 1883 } 1884 } 1885 1886 values.put(MediaStore.Audio.Playlists.NAME, name); 1887 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1888 1889 if (rowId == 0) { 1890 values.put(MediaStore.Audio.Playlists.DATA, path); 1891 uri = mMediaProvider.insert(mPlaylistsUri, values); 1892 rowId = ContentUris.parseId(uri); 1893 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1894 } else { 1895 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1896 mMediaProvider.update(uri, values, null, null); 1897 1898 // delete members of existing playlist 1899 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1900 mMediaProvider.delete(membersUri, null, null); 1901 } 1902 1903 String playListDirectory = path.substring(0, lastSlash + 1); 1904 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1905 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1906 1907 if (fileType == MediaFile.FILE_TYPE_M3U) { 1908 processM3uPlayList(path, playListDirectory, membersUri, values, fileList); 1909 } else if (fileType == MediaFile.FILE_TYPE_PLS) { 1910 processPlsPlayList(path, playListDirectory, membersUri, values, fileList); 1911 } else if (fileType == MediaFile.FILE_TYPE_WPL) { 1912 processWplPlayList(path, playListDirectory, membersUri, values, fileList); 1913 } 1914 } 1915 1916 private void processPlayLists() throws RemoteException { 1917 Iterator<FileEntry> iterator = mPlayLists.iterator(); 1918 Cursor fileList = null; 1919 try { 1920 // use the files uri and projection because we need the format column, 1921 // but restrict the query to just audio files 1922 fileList = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION, 1923 "media_type=2", null, null, null); 1924 while (iterator.hasNext()) { 1925 FileEntry entry = iterator.next(); 1926 // only process playlist files if they are new or have been modified since the last scan 1927 if (entry.mLastModifiedChanged) { 1928 processPlayList(entry, fileList); 1929 } 1930 } 1931 } catch (RemoteException e1) { 1932 } finally { 1933 if (fileList != null) { 1934 fileList.close(); 1935 } 1936 } 1937 } 1938 1939 private native void processDirectory(String path, MediaScannerClient client); 1940 private native void processFile(String path, String mimeType, MediaScannerClient client); 1941 private native void setLocale(String locale); 1942 1943 public native byte[] extractAlbumArt(FileDescriptor fd); 1944 1945 private static native final void native_init(); 1946 private native final void native_setup(); 1947 private native final void native_finalize(); 1948 1949 @Override 1950 public void close() { 1951 mCloseGuard.close(); 1952 if (mClosed.compareAndSet(false, true)) { 1953 mMediaProvider.close(); 1954 native_finalize(); 1955 } 1956 } 1957 1958 @Override 1959 protected void finalize() throws Throwable { 1960 try { 1961 mCloseGuard.warnIfOpen(); 1962 close(); 1963 } finally { 1964 super.finalize(); 1965 } 1966 } 1967 } 1968