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"); native_init()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 FileEntry(long rowId, String path, long lastModified, int format)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 toString()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 MediaScanner(Context c, String volumeName)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 setDefaultRingtoneFileNames()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 isDrmEnabled()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 boolean mScanSuccess; 492 private int mWidth; 493 private int mHeight; 494 MyMediaScannerClient()495 public MyMediaScannerClient() { 496 mDateFormatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss"); 497 mDateFormatter.setTimeZone(TimeZone.getTimeZone("UTC")); 498 } 499 beginFile(String path, String mimeType, long lastModified, long fileSize, boolean isDirectory, boolean noMedia)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 mScanSuccess = true; 507 508 if (!isDirectory) { 509 if (!noMedia && isNoMediaFile(path)) { 510 noMedia = true; 511 } 512 mNoMedia = noMedia; 513 514 // try mimeType first, if it is specified 515 if (mimeType != null) { 516 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 517 } 518 519 // if mimeType was not specified, compute file type based on file extension. 520 if (mFileType == 0) { 521 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 522 if (mediaFileType != null) { 523 mFileType = mediaFileType.fileType; 524 if (mMimeType == null) { 525 mMimeType = mediaFileType.mimeType; 526 } 527 } 528 } 529 530 if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) { 531 mFileType = getFileTypeFromDrm(path); 532 } 533 } 534 535 FileEntry entry = makeEntryFor(path); 536 // add some slack to avoid a rounding error 537 long delta = (entry != null) ? (lastModified - entry.mLastModified) : 0; 538 boolean wasModified = delta > 1 || delta < -1; 539 if (entry == null || wasModified) { 540 if (wasModified) { 541 entry.mLastModified = lastModified; 542 } else { 543 entry = new FileEntry(0, path, lastModified, 544 (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0)); 545 } 546 entry.mLastModifiedChanged = true; 547 } 548 549 if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) { 550 mPlayLists.add(entry); 551 // we don't process playlists in the main scan, so return null 552 return null; 553 } 554 555 // clear all the metadata 556 mArtist = null; 557 mAlbumArtist = null; 558 mAlbum = null; 559 mTitle = null; 560 mComposer = null; 561 mGenre = null; 562 mTrack = 0; 563 mYear = 0; 564 mDuration = 0; 565 mPath = path; 566 mDate = 0; 567 mLastModified = lastModified; 568 mWriter = null; 569 mCompilation = 0; 570 mWidth = 0; 571 mHeight = 0; 572 573 return entry; 574 } 575 576 @Override 577 public void scanFile(String path, long lastModified, long fileSize, 578 boolean isDirectory, boolean noMedia) { 579 // This is the callback funtion from native codes. 580 // Log.v(TAG, "scanFile: "+path); 581 doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia); 582 } 583 584 public Uri doScanFile(String path, String mimeType, long lastModified, 585 long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) { 586 Uri result = null; 587 // long t1 = System.currentTimeMillis(); 588 try { 589 FileEntry entry = beginFile(path, mimeType, lastModified, 590 fileSize, isDirectory, noMedia); 591 592 if (entry == null) { 593 return null; 594 } 595 596 // if this file was just inserted via mtp, set the rowid to zero 597 // (even though it already exists in the database), to trigger 598 // the correct code path for updating its entry 599 if (mMtpObjectHandle != 0) { 600 entry.mRowId = 0; 601 } 602 603 if (entry.mPath != null) { 604 if (((!mDefaultNotificationSet && 605 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) 606 || (!mDefaultRingtoneSet && 607 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) 608 || (!mDefaultAlarmSet && 609 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)))) { 610 Log.w(TAG, "forcing rescan of " + entry.mPath + 611 "since ringtone setting didn't finish"); 612 scanAlways = true; 613 } else if (isSystemSoundWithMetadata(entry.mPath) 614 && !Build.FINGERPRINT.equals(sLastInternalScanFingerprint)) { 615 // file is located on the system partition where the date cannot be trusted: 616 // rescan if the build fingerprint has changed since the last scan. 617 Log.i(TAG, "forcing rescan of " + entry.mPath 618 + " since build fingerprint changed"); 619 scanAlways = true; 620 } 621 } 622 623 // rescan for metadata if file was modified since last scan 624 if (entry != null && (entry.mLastModifiedChanged || scanAlways)) { 625 if (noMedia) { 626 result = endFile(entry, false, false, false, false, false); 627 } else { 628 boolean isaudio = MediaFile.isAudioFileType(mFileType); 629 boolean isvideo = MediaFile.isVideoFileType(mFileType); 630 boolean isimage = MediaFile.isImageFileType(mFileType); 631 632 if (isaudio || isvideo || isimage) { 633 path = Environment.maybeTranslateEmulatedPathToInternal(new File(path)) 634 .getAbsolutePath(); 635 } 636 637 // we only extract metadata for audio and video files 638 if (isaudio || isvideo) { 639 mScanSuccess = processFile(path, mimeType, this); 640 } 641 642 if (isimage) { 643 mScanSuccess = processImageFile(path); 644 } 645 646 String lowpath = path.toLowerCase(Locale.ROOT); 647 boolean ringtones = mScanSuccess && (lowpath.indexOf(RINGTONES_DIR) > 0); 648 boolean notifications = mScanSuccess && 649 (lowpath.indexOf(NOTIFICATIONS_DIR) > 0); 650 boolean alarms = mScanSuccess && (lowpath.indexOf(ALARMS_DIR) > 0); 651 boolean podcasts = mScanSuccess && (lowpath.indexOf(PODCAST_DIR) > 0); 652 boolean music = mScanSuccess && ((lowpath.indexOf(MUSIC_DIR) > 0) || 653 (!ringtones && !notifications && !alarms && !podcasts)); 654 655 result = endFile(entry, ringtones, notifications, alarms, music, podcasts); 656 } 657 } 658 } catch (RemoteException e) { 659 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 660 } 661 // long t2 = System.currentTimeMillis(); 662 // Log.v(TAG, "scanFile: " + path + " took " + (t2-t1)); 663 return result; 664 } 665 666 private long parseDate(String date) { 667 try { 668 return mDateFormatter.parse(date).getTime(); 669 } catch (ParseException e) { 670 return 0; 671 } 672 } 673 674 private int parseSubstring(String s, int start, int defaultValue) { 675 int length = s.length(); 676 if (start == length) return defaultValue; 677 678 char ch = s.charAt(start++); 679 // return defaultValue if we have no integer at all 680 if (ch < '0' || ch > '9') return defaultValue; 681 682 int result = ch - '0'; 683 while (start < length) { 684 ch = s.charAt(start++); 685 if (ch < '0' || ch > '9') return result; 686 result = result * 10 + (ch - '0'); 687 } 688 689 return result; 690 } 691 692 public void handleStringTag(String name, String value) { 693 if (name.equalsIgnoreCase("title") || name.startsWith("title;")) { 694 // Don't trim() here, to preserve the special \001 character 695 // used to force sorting. The media provider will trim() before 696 // inserting the title in to the database. 697 mTitle = value; 698 } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) { 699 mArtist = value.trim(); 700 } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;") 701 || name.equalsIgnoreCase("band") || name.startsWith("band;")) { 702 mAlbumArtist = value.trim(); 703 } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) { 704 mAlbum = value.trim(); 705 } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) { 706 mComposer = value.trim(); 707 } else if (mProcessGenres && 708 (name.equalsIgnoreCase("genre") || name.startsWith("genre;"))) { 709 mGenre = getGenreName(value); 710 } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) { 711 mYear = parseSubstring(value, 0, 0); 712 } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) { 713 // track number might be of the form "2/12" 714 // we just read the number before the slash 715 int num = parseSubstring(value, 0, 0); 716 mTrack = (mTrack / 1000) * 1000 + num; 717 } else if (name.equalsIgnoreCase("discnumber") || 718 name.equals("set") || name.startsWith("set;")) { 719 // set number might be of the form "1/3" 720 // we just read the number before the slash 721 int num = parseSubstring(value, 0, 0); 722 mTrack = (num * 1000) + (mTrack % 1000); 723 } else if (name.equalsIgnoreCase("duration")) { 724 mDuration = parseSubstring(value, 0, 0); 725 } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) { 726 mWriter = value.trim(); 727 } else if (name.equalsIgnoreCase("compilation")) { 728 mCompilation = parseSubstring(value, 0, 0); 729 } else if (name.equalsIgnoreCase("isdrm")) { 730 mIsDrm = (parseSubstring(value, 0, 0) == 1); 731 } else if (name.equalsIgnoreCase("date")) { 732 mDate = parseDate(value); 733 } else if (name.equalsIgnoreCase("width")) { 734 mWidth = parseSubstring(value, 0, 0); 735 } else if (name.equalsIgnoreCase("height")) { 736 mHeight = parseSubstring(value, 0, 0); 737 } else { 738 //Log.v(TAG, "unknown tag: " + name + " (" + mProcessGenres + ")"); 739 } 740 } 741 742 private boolean convertGenreCode(String input, String expected) { 743 String output = getGenreName(input); 744 if (output.equals(expected)) { 745 return true; 746 } else { 747 Log.d(TAG, "'" + input + "' -> '" + output + "', expected '" + expected + "'"); 748 return false; 749 } 750 } 751 private void testGenreNameConverter() { 752 convertGenreCode("2", "Country"); 753 convertGenreCode("(2)", "Country"); 754 convertGenreCode("(2", "(2"); 755 convertGenreCode("2 Foo", "Country"); 756 convertGenreCode("(2) Foo", "Country"); 757 convertGenreCode("(2 Foo", "(2 Foo"); 758 convertGenreCode("2Foo", "2Foo"); 759 convertGenreCode("(2)Foo", "Country"); 760 convertGenreCode("200 Foo", "Foo"); 761 convertGenreCode("(200) Foo", "Foo"); 762 convertGenreCode("200Foo", "200Foo"); 763 convertGenreCode("(200)Foo", "Foo"); 764 convertGenreCode("200)Foo", "200)Foo"); 765 convertGenreCode("200) Foo", "200) Foo"); 766 } 767 768 public String getGenreName(String genreTagValue) { 769 770 if (genreTagValue == null) { 771 return null; 772 } 773 final int length = genreTagValue.length(); 774 775 if (length > 0) { 776 boolean parenthesized = false; 777 StringBuffer number = new StringBuffer(); 778 int i = 0; 779 for (; i < length; ++i) { 780 char c = genreTagValue.charAt(i); 781 if (i == 0 && c == '(') { 782 parenthesized = true; 783 } else if (Character.isDigit(c)) { 784 number.append(c); 785 } else { 786 break; 787 } 788 } 789 char charAfterNumber = i < length ? genreTagValue.charAt(i) : ' '; 790 if ((parenthesized && charAfterNumber == ')') 791 || !parenthesized && Character.isWhitespace(charAfterNumber)) { 792 try { 793 short genreIndex = Short.parseShort(number.toString()); 794 if (genreIndex >= 0) { 795 if (genreIndex < ID3_GENRES.length && ID3_GENRES[genreIndex] != null) { 796 return ID3_GENRES[genreIndex]; 797 } else if (genreIndex == 0xFF) { 798 return null; 799 } else if (genreIndex < 0xFF && (i + 1) < length) { 800 // genre is valid but unknown, 801 // if there is a string after the value we take it 802 if (parenthesized && charAfterNumber == ')') { 803 i++; 804 } 805 String ret = genreTagValue.substring(i).trim(); 806 if (ret.length() != 0) { 807 return ret; 808 } 809 } else { 810 // else return the number, without parentheses 811 return number.toString(); 812 } 813 } 814 } catch (NumberFormatException e) { 815 } 816 } 817 } 818 819 return genreTagValue; 820 } 821 822 private boolean processImageFile(String path) { 823 try { 824 mBitmapOptions.outWidth = 0; 825 mBitmapOptions.outHeight = 0; 826 BitmapFactory.decodeFile(path, mBitmapOptions); 827 mWidth = mBitmapOptions.outWidth; 828 mHeight = mBitmapOptions.outHeight; 829 return mWidth > 0 && mHeight > 0; 830 } catch (Throwable th) { 831 // ignore; 832 } 833 return false; 834 } 835 836 public void setMimeType(String mimeType) { 837 if ("audio/mp4".equals(mMimeType) && 838 mimeType.startsWith("video")) { 839 // for feature parity with Donut, we force m4a files to keep the 840 // audio/mp4 mimetype, even if they are really "enhanced podcasts" 841 // with a video track 842 return; 843 } 844 mMimeType = mimeType; 845 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 846 } 847 848 /** 849 * Formats the data into a values array suitable for use with the Media 850 * Content Provider. 851 * 852 * @return a map of values 853 */ 854 private ContentValues toValues() { 855 ContentValues map = new ContentValues(); 856 857 map.put(MediaStore.MediaColumns.DATA, mPath); 858 map.put(MediaStore.MediaColumns.TITLE, mTitle); 859 map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified); 860 map.put(MediaStore.MediaColumns.SIZE, mFileSize); 861 map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType); 862 map.put(MediaStore.MediaColumns.IS_DRM, mIsDrm); 863 864 String resolution = null; 865 if (mWidth > 0 && mHeight > 0) { 866 map.put(MediaStore.MediaColumns.WIDTH, mWidth); 867 map.put(MediaStore.MediaColumns.HEIGHT, mHeight); 868 resolution = mWidth + "x" + mHeight; 869 } 870 871 if (!mNoMedia) { 872 if (MediaFile.isVideoFileType(mFileType)) { 873 map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 874 ? mArtist : MediaStore.UNKNOWN_STRING)); 875 map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 876 ? mAlbum : MediaStore.UNKNOWN_STRING)); 877 map.put(Video.Media.DURATION, mDuration); 878 if (resolution != null) { 879 map.put(Video.Media.RESOLUTION, resolution); 880 } 881 if (mDate > 0) { 882 map.put(Video.Media.DATE_TAKEN, mDate); 883 } 884 } else if (MediaFile.isImageFileType(mFileType)) { 885 // FIXME - add DESCRIPTION 886 } else if (mScanSuccess && MediaFile.isAudioFileType(mFileType)) { 887 map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ? 888 mArtist : MediaStore.UNKNOWN_STRING); 889 map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null && 890 mAlbumArtist.length() > 0) ? mAlbumArtist : null); 891 map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ? 892 mAlbum : MediaStore.UNKNOWN_STRING); 893 map.put(Audio.Media.COMPOSER, mComposer); 894 map.put(Audio.Media.GENRE, mGenre); 895 if (mYear != 0) { 896 map.put(Audio.Media.YEAR, mYear); 897 } 898 map.put(Audio.Media.TRACK, mTrack); 899 map.put(Audio.Media.DURATION, mDuration); 900 map.put(Audio.Media.COMPILATION, mCompilation); 901 } 902 if (!mScanSuccess) { 903 // force mediaprovider to not determine the media type from the mime type 904 map.put(Files.FileColumns.MEDIA_TYPE, 0); 905 } 906 } 907 return map; 908 } 909 910 private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications, 911 boolean alarms, boolean music, boolean podcasts) 912 throws RemoteException { 913 // update database 914 915 // use album artist if artist is missing 916 if (mArtist == null || mArtist.length() == 0) { 917 mArtist = mAlbumArtist; 918 } 919 920 ContentValues values = toValues(); 921 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 922 if (title == null || TextUtils.isEmpty(title.trim())) { 923 title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA)); 924 values.put(MediaStore.MediaColumns.TITLE, title); 925 } 926 String album = values.getAsString(Audio.Media.ALBUM); 927 if (MediaStore.UNKNOWN_STRING.equals(album)) { 928 album = values.getAsString(MediaStore.MediaColumns.DATA); 929 // extract last path segment before file name 930 int lastSlash = album.lastIndexOf('/'); 931 if (lastSlash >= 0) { 932 int previousSlash = 0; 933 while (true) { 934 int idx = album.indexOf('/', previousSlash + 1); 935 if (idx < 0 || idx >= lastSlash) { 936 break; 937 } 938 previousSlash = idx; 939 } 940 if (previousSlash != 0) { 941 album = album.substring(previousSlash + 1, lastSlash); 942 values.put(Audio.Media.ALBUM, album); 943 } 944 } 945 } 946 long rowId = entry.mRowId; 947 if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) { 948 // Only set these for new entries. For existing entries, they 949 // may have been modified later, and we want to keep the current 950 // values so that custom ringtones still show up in the ringtone 951 // picker. 952 values.put(Audio.Media.IS_RINGTONE, ringtones); 953 values.put(Audio.Media.IS_NOTIFICATION, notifications); 954 values.put(Audio.Media.IS_ALARM, alarms); 955 values.put(Audio.Media.IS_MUSIC, music); 956 values.put(Audio.Media.IS_PODCAST, podcasts); 957 } else if ((mFileType == MediaFile.FILE_TYPE_JPEG 958 || mFileType == MediaFile.FILE_TYPE_HEIF 959 || MediaFile.isRawImageFileType(mFileType)) && !mNoMedia) { 960 ExifInterface exif = null; 961 try { 962 exif = new ExifInterface(entry.mPath); 963 } catch (IOException ex) { 964 // exif is null 965 } 966 if (exif != null) { 967 float[] latlng = new float[2]; 968 if (exif.getLatLong(latlng)) { 969 values.put(Images.Media.LATITUDE, latlng[0]); 970 values.put(Images.Media.LONGITUDE, latlng[1]); 971 } 972 973 long time = exif.getGpsDateTime(); 974 if (time != -1) { 975 values.put(Images.Media.DATE_TAKEN, time); 976 } else { 977 // If no time zone information is available, we should consider using 978 // EXIF local time as taken time if the difference between file time 979 // and EXIF local time is not less than 1 Day, otherwise MediaProvider 980 // will use file time as taken time. 981 time = exif.getDateTime(); 982 if (time != -1 && Math.abs(mLastModified * 1000 - time) >= 86400000) { 983 values.put(Images.Media.DATE_TAKEN, time); 984 } 985 } 986 987 int orientation = exif.getAttributeInt( 988 ExifInterface.TAG_ORIENTATION, -1); 989 if (orientation != -1) { 990 // We only recognize a subset of orientation tag values. 991 int degree; 992 switch(orientation) { 993 case ExifInterface.ORIENTATION_ROTATE_90: 994 degree = 90; 995 break; 996 case ExifInterface.ORIENTATION_ROTATE_180: 997 degree = 180; 998 break; 999 case ExifInterface.ORIENTATION_ROTATE_270: 1000 degree = 270; 1001 break; 1002 default: 1003 degree = 0; 1004 break; 1005 } 1006 values.put(Images.Media.ORIENTATION, degree); 1007 } 1008 } 1009 } 1010 1011 Uri tableUri = mFilesUri; 1012 MediaInserter inserter = mMediaInserter; 1013 if (mScanSuccess && !mNoMedia) { 1014 if (MediaFile.isVideoFileType(mFileType)) { 1015 tableUri = mVideoUri; 1016 } else if (MediaFile.isImageFileType(mFileType)) { 1017 tableUri = mImagesUri; 1018 } else if (MediaFile.isAudioFileType(mFileType)) { 1019 tableUri = mAudioUri; 1020 } 1021 } 1022 Uri result = null; 1023 boolean needToSetSettings = false; 1024 // Setting a flag in order not to use bulk insert for the file related with 1025 // notifications, ringtones, and alarms, because the rowId of the inserted file is 1026 // needed. 1027 if (notifications && !mDefaultNotificationSet) { 1028 if (TextUtils.isEmpty(mDefaultNotificationFilename) || 1029 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) { 1030 needToSetSettings = true; 1031 } 1032 } else if (ringtones && !mDefaultRingtoneSet) { 1033 if (TextUtils.isEmpty(mDefaultRingtoneFilename) || 1034 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) { 1035 needToSetSettings = true; 1036 } 1037 } else if (alarms && !mDefaultAlarmSet) { 1038 if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) || 1039 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) { 1040 needToSetSettings = true; 1041 } 1042 } 1043 1044 if (rowId == 0) { 1045 if (mMtpObjectHandle != 0) { 1046 values.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle); 1047 } 1048 if (tableUri == mFilesUri) { 1049 int format = entry.mFormat; 1050 if (format == 0) { 1051 format = MediaFile.getFormatCode(entry.mPath, mMimeType); 1052 } 1053 values.put(Files.FileColumns.FORMAT, format); 1054 } 1055 // New file, insert it. 1056 // Directories need to be inserted before the files they contain, so they 1057 // get priority when bulk inserting. 1058 // If the rowId of the inserted file is needed, it gets inserted immediately, 1059 // bypassing the bulk inserter. 1060 if (inserter == null || needToSetSettings) { 1061 if (inserter != null) { 1062 inserter.flushAll(); 1063 } 1064 result = mMediaProvider.insert(tableUri, values); 1065 } else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) { 1066 inserter.insertwithPriority(tableUri, values); 1067 } else { 1068 inserter.insert(tableUri, values); 1069 } 1070 1071 if (result != null) { 1072 rowId = ContentUris.parseId(result); 1073 entry.mRowId = rowId; 1074 } 1075 } else { 1076 // updated file 1077 result = ContentUris.withAppendedId(tableUri, rowId); 1078 // path should never change, and we want to avoid replacing mixed cased paths 1079 // with squashed lower case paths 1080 values.remove(MediaStore.MediaColumns.DATA); 1081 1082 int mediaType = 0; 1083 if (mScanSuccess && !MediaScanner.isNoMediaPath(entry.mPath)) { 1084 int fileType = MediaFile.getFileTypeForMimeType(mMimeType); 1085 if (MediaFile.isAudioFileType(fileType)) { 1086 mediaType = FileColumns.MEDIA_TYPE_AUDIO; 1087 } else if (MediaFile.isVideoFileType(fileType)) { 1088 mediaType = FileColumns.MEDIA_TYPE_VIDEO; 1089 } else if (MediaFile.isImageFileType(fileType)) { 1090 mediaType = FileColumns.MEDIA_TYPE_IMAGE; 1091 } else if (MediaFile.isPlayListFileType(fileType)) { 1092 mediaType = FileColumns.MEDIA_TYPE_PLAYLIST; 1093 } 1094 values.put(FileColumns.MEDIA_TYPE, mediaType); 1095 } 1096 mMediaProvider.update(result, values, null, null); 1097 } 1098 1099 if(needToSetSettings) { 1100 if (notifications) { 1101 setRingtoneIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId); 1102 mDefaultNotificationSet = true; 1103 } else if (ringtones) { 1104 setRingtoneIfNotSet(Settings.System.RINGTONE, tableUri, rowId); 1105 mDefaultRingtoneSet = true; 1106 } else if (alarms) { 1107 setRingtoneIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId); 1108 mDefaultAlarmSet = true; 1109 } 1110 } 1111 1112 return result; 1113 } 1114 1115 private boolean doesPathHaveFilename(String path, String filename) { 1116 int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1; 1117 int filenameLength = filename.length(); 1118 return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) && 1119 pathFilenameStart + filenameLength == path.length(); 1120 } 1121 1122 private void setRingtoneIfNotSet(String settingName, Uri uri, long rowId) { 1123 if (wasRingtoneAlreadySet(settingName)) { 1124 return; 1125 } 1126 1127 ContentResolver cr = mContext.getContentResolver(); 1128 String existingSettingValue = Settings.System.getString(cr, settingName); 1129 if (TextUtils.isEmpty(existingSettingValue)) { 1130 final Uri settingUri = Settings.System.getUriFor(settingName); 1131 final Uri ringtoneUri = ContentUris.withAppendedId(uri, rowId); 1132 RingtoneManager.setActualDefaultRingtoneUri(mContext, 1133 RingtoneManager.getDefaultType(settingUri), ringtoneUri); 1134 } 1135 Settings.System.putInt(cr, settingSetIndicatorName(settingName), 1); 1136 } 1137 1138 private int getFileTypeFromDrm(String path) { 1139 if (!isDrmEnabled()) { 1140 return 0; 1141 } 1142 1143 int resultFileType = 0; 1144 1145 if (mDrmManagerClient == null) { 1146 mDrmManagerClient = new DrmManagerClient(mContext); 1147 } 1148 1149 if (mDrmManagerClient.canHandle(path, null)) { 1150 mIsDrm = true; 1151 String drmMimetype = mDrmManagerClient.getOriginalMimeType(path); 1152 if (drmMimetype != null) { 1153 mMimeType = drmMimetype; 1154 resultFileType = MediaFile.getFileTypeForMimeType(drmMimetype); 1155 } 1156 } 1157 return resultFileType; 1158 } 1159 1160 }; // end of anonymous MediaScannerClient instance 1161 1162 private static boolean isSystemSoundWithMetadata(String path) { 1163 if (path.startsWith(SYSTEM_SOUNDS_DIR + ALARMS_DIR) 1164 || path.startsWith(SYSTEM_SOUNDS_DIR + RINGTONES_DIR) 1165 || path.startsWith(SYSTEM_SOUNDS_DIR + NOTIFICATIONS_DIR) 1166 || path.startsWith(PRODUCT_SOUNDS_DIR + ALARMS_DIR) 1167 || path.startsWith(PRODUCT_SOUNDS_DIR + RINGTONES_DIR) 1168 || path.startsWith(PRODUCT_SOUNDS_DIR + NOTIFICATIONS_DIR)) { 1169 return true; 1170 } 1171 return false; 1172 } 1173 1174 private String settingSetIndicatorName(String base) { 1175 return base + "_set"; 1176 } 1177 1178 private boolean wasRingtoneAlreadySet(String name) { 1179 ContentResolver cr = mContext.getContentResolver(); 1180 String indicatorName = settingSetIndicatorName(name); 1181 try { 1182 return Settings.System.getInt(cr, indicatorName) != 0; 1183 } catch (SettingNotFoundException e) { 1184 return false; 1185 } 1186 } 1187 1188 private void prescan(String filePath, boolean prescanFiles) throws RemoteException { 1189 Cursor c = null; 1190 String where = null; 1191 String[] selectionArgs = null; 1192 1193 mPlayLists.clear(); 1194 1195 if (filePath != null) { 1196 // query for only one file 1197 where = MediaStore.Files.FileColumns._ID + ">?" + 1198 " AND " + Files.FileColumns.DATA + "=?"; 1199 selectionArgs = new String[] { "", filePath }; 1200 } else { 1201 where = MediaStore.Files.FileColumns._ID + ">?"; 1202 selectionArgs = new String[] { "" }; 1203 } 1204 1205 mDefaultRingtoneSet = wasRingtoneAlreadySet(Settings.System.RINGTONE); 1206 mDefaultNotificationSet = wasRingtoneAlreadySet(Settings.System.NOTIFICATION_SOUND); 1207 mDefaultAlarmSet = wasRingtoneAlreadySet(Settings.System.ALARM_ALERT); 1208 1209 // Tell the provider to not delete the file. 1210 // If the file is truly gone the delete is unnecessary, and we want to avoid 1211 // accidentally deleting files that are really there (this may happen if the 1212 // filesystem is mounted and unmounted while the scanner is running). 1213 Uri.Builder builder = mFilesUri.buildUpon(); 1214 builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false"); 1215 MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, builder.build()); 1216 1217 // Build the list of files from the content provider 1218 try { 1219 if (prescanFiles) { 1220 // First read existing files from the files table. 1221 // Because we'll be deleting entries for missing files as we go, 1222 // we need to query the database in small batches, to avoid problems 1223 // with CursorWindow positioning. 1224 long lastId = Long.MIN_VALUE; 1225 Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build(); 1226 1227 while (true) { 1228 selectionArgs[0] = "" + lastId; 1229 if (c != null) { 1230 c.close(); 1231 c = null; 1232 } 1233 c = mMediaProvider.query(limitUri, FILES_PRESCAN_PROJECTION, 1234 where, selectionArgs, MediaStore.Files.FileColumns._ID, null); 1235 if (c == null) { 1236 break; 1237 } 1238 1239 int num = c.getCount(); 1240 1241 if (num == 0) { 1242 break; 1243 } 1244 while (c.moveToNext()) { 1245 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1246 String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); 1247 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1248 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1249 lastId = rowId; 1250 1251 // Only consider entries with absolute path names. 1252 // This allows storing URIs in the database without the 1253 // media scanner removing them. 1254 if (path != null && path.startsWith("/")) { 1255 boolean exists = false; 1256 try { 1257 exists = Os.access(path, android.system.OsConstants.F_OK); 1258 } catch (ErrnoException e1) { 1259 } 1260 if (!exists && !MtpConstants.isAbstractObject(format)) { 1261 // do not delete missing playlists, since they may have been 1262 // modified by the user. 1263 // The user can delete them in the media player instead. 1264 // instead, clear the path and lastModified fields in the row 1265 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1266 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1267 1268 if (!MediaFile.isPlayListFileType(fileType)) { 1269 deleter.delete(rowId); 1270 if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) { 1271 deleter.flush(); 1272 String parent = new File(path).getParent(); 1273 mMediaProvider.call(MediaStore.UNHIDE_CALL, parent, null); 1274 } 1275 } 1276 } 1277 } 1278 } 1279 } 1280 } 1281 } 1282 finally { 1283 if (c != null) { 1284 c.close(); 1285 } 1286 deleter.flush(); 1287 } 1288 1289 // compute original size of images 1290 mOriginalCount = 0; 1291 c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null, null); 1292 if (c != null) { 1293 mOriginalCount = c.getCount(); 1294 c.close(); 1295 } 1296 } 1297 1298 static class MediaBulkDeleter { 1299 StringBuilder whereClause = new StringBuilder(); 1300 ArrayList<String> whereArgs = new ArrayList<String>(100); 1301 final ContentProviderClient mProvider; 1302 final Uri mBaseUri; 1303 1304 public MediaBulkDeleter(ContentProviderClient provider, Uri baseUri) { 1305 mProvider = provider; 1306 mBaseUri = baseUri; 1307 } 1308 1309 public void delete(long id) throws RemoteException { 1310 if (whereClause.length() != 0) { 1311 whereClause.append(","); 1312 } 1313 whereClause.append("?"); 1314 whereArgs.add("" + id); 1315 if (whereArgs.size() > 100) { 1316 flush(); 1317 } 1318 } 1319 public void flush() throws RemoteException { 1320 int size = whereArgs.size(); 1321 if (size > 0) { 1322 String [] foo = new String [size]; 1323 foo = whereArgs.toArray(foo); 1324 int numrows = mProvider.delete(mBaseUri, 1325 MediaStore.MediaColumns._ID + " IN (" + 1326 whereClause.toString() + ")", foo); 1327 //Log.i("@@@@@@@@@", "rows deleted: " + numrows); 1328 whereClause.setLength(0); 1329 whereArgs.clear(); 1330 } 1331 } 1332 } 1333 1334 private void postscan(final String[] directories) throws RemoteException { 1335 1336 // handle playlists last, after we know what media files are on the storage. 1337 if (mProcessPlaylists) { 1338 processPlayLists(); 1339 } 1340 1341 // allow GC to clean up 1342 mPlayLists.clear(); 1343 } 1344 1345 private void releaseResources() { 1346 // release the DrmManagerClient resources 1347 if (mDrmManagerClient != null) { 1348 mDrmManagerClient.close(); 1349 mDrmManagerClient = null; 1350 } 1351 } 1352 1353 public void scanDirectories(String[] directories) { 1354 try { 1355 long start = System.currentTimeMillis(); 1356 prescan(null, true); 1357 long prescan = System.currentTimeMillis(); 1358 1359 if (ENABLE_BULK_INSERTS) { 1360 // create MediaInserter for bulk inserts 1361 mMediaInserter = new MediaInserter(mMediaProvider, 500); 1362 } 1363 1364 for (int i = 0; i < directories.length; i++) { 1365 processDirectory(directories[i], mClient); 1366 } 1367 1368 if (ENABLE_BULK_INSERTS) { 1369 // flush remaining inserts 1370 mMediaInserter.flushAll(); 1371 mMediaInserter = null; 1372 } 1373 1374 long scan = System.currentTimeMillis(); 1375 postscan(directories); 1376 long end = System.currentTimeMillis(); 1377 1378 if (false) { 1379 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1380 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1381 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1382 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1383 } 1384 } catch (SQLException e) { 1385 // this might happen if the SD card is removed while the media scanner is running 1386 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1387 } catch (UnsupportedOperationException e) { 1388 // this might happen if the SD card is removed while the media scanner is running 1389 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1390 } catch (RemoteException e) { 1391 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1392 } finally { 1393 releaseResources(); 1394 } 1395 } 1396 1397 // this function is used to scan a single file 1398 public Uri scanSingleFile(String path, String mimeType) { 1399 try { 1400 prescan(path, true); 1401 1402 File file = new File(path); 1403 if (!file.exists() || !file.canRead()) { 1404 return null; 1405 } 1406 1407 // lastModified is in milliseconds on Files. 1408 long lastModifiedSeconds = file.lastModified() / 1000; 1409 1410 // always scan the file, so we can return the content://media Uri for existing files 1411 return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(), 1412 false, true, MediaScanner.isNoMediaPath(path)); 1413 } catch (RemoteException e) { 1414 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1415 return null; 1416 } finally { 1417 releaseResources(); 1418 } 1419 } 1420 1421 private static boolean isNoMediaFile(String path) { 1422 File file = new File(path); 1423 if (file.isDirectory()) return false; 1424 1425 // special case certain file names 1426 // I use regionMatches() instead of substring() below 1427 // to avoid memory allocation 1428 int lastSlash = path.lastIndexOf('/'); 1429 if (lastSlash >= 0 && lastSlash + 2 < path.length()) { 1430 // ignore those ._* files created by MacOS 1431 if (path.regionMatches(lastSlash + 1, "._", 0, 2)) { 1432 return true; 1433 } 1434 1435 // ignore album art files created by Windows Media Player: 1436 // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg 1437 // and AlbumArt_{...}_Small.jpg 1438 if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) { 1439 if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) || 1440 path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) { 1441 return true; 1442 } 1443 int length = path.length() - lastSlash - 1; 1444 if ((length == 17 && path.regionMatches( 1445 true, lastSlash + 1, "AlbumArtSmall", 0, 13)) || 1446 (length == 10 1447 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) { 1448 return true; 1449 } 1450 } 1451 } 1452 return false; 1453 } 1454 1455 private static HashMap<String,String> mNoMediaPaths = new HashMap<String,String>(); 1456 private static HashMap<String,String> mMediaPaths = new HashMap<String,String>(); 1457 1458 /* MediaProvider calls this when a .nomedia file is added or removed */ 1459 public static void clearMediaPathCache(boolean clearMediaPaths, boolean clearNoMediaPaths) { 1460 synchronized (MediaScanner.class) { 1461 if (clearMediaPaths) { 1462 mMediaPaths.clear(); 1463 } 1464 if (clearNoMediaPaths) { 1465 mNoMediaPaths.clear(); 1466 } 1467 } 1468 } 1469 1470 public static boolean isNoMediaPath(String path) { 1471 if (path == null) { 1472 return false; 1473 } 1474 // return true if file or any parent directory has name starting with a dot 1475 if (path.indexOf("/.") >= 0) { 1476 return true; 1477 } 1478 1479 int firstSlash = path.lastIndexOf('/'); 1480 if (firstSlash <= 0) { 1481 return false; 1482 } 1483 String parent = path.substring(0, firstSlash); 1484 1485 synchronized (MediaScanner.class) { 1486 if (mNoMediaPaths.containsKey(parent)) { 1487 return true; 1488 } else if (!mMediaPaths.containsKey(parent)) { 1489 // check to see if any parent directories have a ".nomedia" file 1490 // start from 1 so we don't bother checking in the root directory 1491 int offset = 1; 1492 while (offset >= 0) { 1493 int slashIndex = path.indexOf('/', offset); 1494 if (slashIndex > offset) { 1495 slashIndex++; // move past slash 1496 File file = new File(path.substring(0, slashIndex) + ".nomedia"); 1497 if (file.exists()) { 1498 // we have a .nomedia in one of the parent directories 1499 mNoMediaPaths.put(parent, ""); 1500 return true; 1501 } 1502 } 1503 offset = slashIndex; 1504 } 1505 mMediaPaths.put(parent, ""); 1506 } 1507 } 1508 1509 return isNoMediaFile(path); 1510 } 1511 1512 public void scanMtpFile(String path, int objectHandle, int format) { 1513 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1514 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1515 File file = new File(path); 1516 long lastModifiedSeconds = file.lastModified() / 1000; 1517 1518 if (!MediaFile.isAudioFileType(fileType) && !MediaFile.isVideoFileType(fileType) && 1519 !MediaFile.isImageFileType(fileType) && !MediaFile.isPlayListFileType(fileType) && 1520 !MediaFile.isDrmFileType(fileType)) { 1521 1522 // no need to use the media scanner, but we need to update last modified and file size 1523 ContentValues values = new ContentValues(); 1524 values.put(Files.FileColumns.SIZE, file.length()); 1525 values.put(Files.FileColumns.DATE_MODIFIED, lastModifiedSeconds); 1526 try { 1527 String[] whereArgs = new String[] { Integer.toString(objectHandle) }; 1528 mMediaProvider.update(Files.getMtpObjectsUri(mVolumeName), values, 1529 "_id=?", whereArgs); 1530 } catch (RemoteException e) { 1531 Log.e(TAG, "RemoteException in scanMtpFile", e); 1532 } 1533 return; 1534 } 1535 1536 mMtpObjectHandle = objectHandle; 1537 Cursor fileList = null; 1538 try { 1539 if (MediaFile.isPlayListFileType(fileType)) { 1540 // build file cache so we can look up tracks in the playlist 1541 prescan(null, true); 1542 1543 FileEntry entry = makeEntryFor(path); 1544 if (entry != null) { 1545 fileList = mMediaProvider.query(mFilesUri, 1546 FILES_PRESCAN_PROJECTION, null, null, null, null); 1547 processPlayList(entry, fileList); 1548 } 1549 } else { 1550 // MTP will create a file entry for us so we don't want to do it in prescan 1551 prescan(path, false); 1552 1553 // always scan the file, so we can return the content://media Uri for existing files 1554 mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(), 1555 (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path)); 1556 } 1557 } catch (RemoteException e) { 1558 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1559 } finally { 1560 mMtpObjectHandle = 0; 1561 if (fileList != null) { 1562 fileList.close(); 1563 } 1564 releaseResources(); 1565 } 1566 } 1567 1568 FileEntry makeEntryFor(String path) { 1569 String where; 1570 String[] selectionArgs; 1571 1572 Cursor c = null; 1573 try { 1574 where = Files.FileColumns.DATA + "=?"; 1575 selectionArgs = new String[] { path }; 1576 c = mMediaProvider.query(mFilesUriNoNotify, FILES_PRESCAN_PROJECTION, 1577 where, selectionArgs, null, null); 1578 if (c.moveToFirst()) { 1579 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1580 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1581 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1582 return new FileEntry(rowId, path, lastModified, format); 1583 } 1584 } catch (RemoteException e) { 1585 } finally { 1586 if (c != null) { 1587 c.close(); 1588 } 1589 } 1590 return null; 1591 } 1592 1593 // returns the number of matching file/directory names, starting from the right 1594 private int matchPaths(String path1, String path2) { 1595 int result = 0; 1596 int end1 = path1.length(); 1597 int end2 = path2.length(); 1598 1599 while (end1 > 0 && end2 > 0) { 1600 int slash1 = path1.lastIndexOf('/', end1 - 1); 1601 int slash2 = path2.lastIndexOf('/', end2 - 1); 1602 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1603 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1604 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1605 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1606 if (start1 < 0) start1 = 0; else start1++; 1607 if (start2 < 0) start2 = 0; else start2++; 1608 int length = end1 - start1; 1609 if (end2 - start2 != length) break; 1610 if (path1.regionMatches(true, start1, path2, start2, length)) { 1611 result++; 1612 end1 = start1 - 1; 1613 end2 = start2 - 1; 1614 } else break; 1615 } 1616 1617 return result; 1618 } 1619 1620 private boolean matchEntries(long rowId, String data) { 1621 1622 int len = mPlaylistEntries.size(); 1623 boolean done = true; 1624 for (int i = 0; i < len; i++) { 1625 PlaylistEntry entry = mPlaylistEntries.get(i); 1626 if (entry.bestmatchlevel == Integer.MAX_VALUE) { 1627 continue; // this entry has been matched already 1628 } 1629 done = false; 1630 if (data.equalsIgnoreCase(entry.path)) { 1631 entry.bestmatchid = rowId; 1632 entry.bestmatchlevel = Integer.MAX_VALUE; 1633 continue; // no need for path matching 1634 } 1635 1636 int matchLength = matchPaths(data, entry.path); 1637 if (matchLength > entry.bestmatchlevel) { 1638 entry.bestmatchid = rowId; 1639 entry.bestmatchlevel = matchLength; 1640 } 1641 } 1642 return done; 1643 } 1644 1645 private void cachePlaylistEntry(String line, String playListDirectory) { 1646 PlaylistEntry entry = new PlaylistEntry(); 1647 // watch for trailing whitespace 1648 int entryLength = line.length(); 1649 while (entryLength > 0 && Character.isWhitespace(line.charAt(entryLength - 1))) entryLength--; 1650 // path should be longer than 3 characters. 1651 // avoid index out of bounds errors below by returning here. 1652 if (entryLength < 3) return; 1653 if (entryLength < line.length()) line = line.substring(0, entryLength); 1654 1655 // does entry appear to be an absolute path? 1656 // look for Unix or DOS absolute paths 1657 char ch1 = line.charAt(0); 1658 boolean fullPath = (ch1 == '/' || 1659 (Character.isLetter(ch1) && line.charAt(1) == ':' && line.charAt(2) == '\\')); 1660 // if we have a relative path, combine entry with playListDirectory 1661 if (!fullPath) 1662 line = playListDirectory + line; 1663 entry.path = line; 1664 //FIXME - should we look for "../" within the path? 1665 1666 mPlaylistEntries.add(entry); 1667 } 1668 1669 private void processCachedPlaylist(Cursor fileList, ContentValues values, Uri playlistUri) { 1670 fileList.moveToPosition(-1); 1671 while (fileList.moveToNext()) { 1672 long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1673 String data = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); 1674 if (matchEntries(rowId, data)) { 1675 break; 1676 } 1677 } 1678 1679 int len = mPlaylistEntries.size(); 1680 int index = 0; 1681 for (int i = 0; i < len; i++) { 1682 PlaylistEntry entry = mPlaylistEntries.get(i); 1683 if (entry.bestmatchlevel > 0) { 1684 try { 1685 values.clear(); 1686 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1687 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(entry.bestmatchid)); 1688 mMediaProvider.insert(playlistUri, values); 1689 index++; 1690 } catch (RemoteException e) { 1691 Log.e(TAG, "RemoteException in MediaScanner.processCachedPlaylist()", e); 1692 return; 1693 } 1694 } 1695 } 1696 mPlaylistEntries.clear(); 1697 } 1698 1699 private void processM3uPlayList(String path, String playListDirectory, Uri uri, 1700 ContentValues values, Cursor fileList) { 1701 BufferedReader reader = null; 1702 try { 1703 File f = new File(path); 1704 if (f.exists()) { 1705 reader = new BufferedReader( 1706 new InputStreamReader(new FileInputStream(f)), 8192); 1707 String line = reader.readLine(); 1708 mPlaylistEntries.clear(); 1709 while (line != null) { 1710 // ignore comment lines, which begin with '#' 1711 if (line.length() > 0 && line.charAt(0) != '#') { 1712 cachePlaylistEntry(line, playListDirectory); 1713 } 1714 line = reader.readLine(); 1715 } 1716 1717 processCachedPlaylist(fileList, values, uri); 1718 } 1719 } catch (IOException e) { 1720 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1721 } finally { 1722 try { 1723 if (reader != null) 1724 reader.close(); 1725 } catch (IOException e) { 1726 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1727 } 1728 } 1729 } 1730 1731 private void processPlsPlayList(String path, String playListDirectory, Uri uri, 1732 ContentValues values, Cursor fileList) { 1733 BufferedReader reader = null; 1734 try { 1735 File f = new File(path); 1736 if (f.exists()) { 1737 reader = new BufferedReader( 1738 new InputStreamReader(new FileInputStream(f)), 8192); 1739 String line = reader.readLine(); 1740 mPlaylistEntries.clear(); 1741 while (line != null) { 1742 // ignore comment lines, which begin with '#' 1743 if (line.startsWith("File")) { 1744 int equals = line.indexOf('='); 1745 if (equals > 0) { 1746 cachePlaylistEntry(line.substring(equals + 1), playListDirectory); 1747 } 1748 } 1749 line = reader.readLine(); 1750 } 1751 1752 processCachedPlaylist(fileList, values, uri); 1753 } 1754 } catch (IOException e) { 1755 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1756 } finally { 1757 try { 1758 if (reader != null) 1759 reader.close(); 1760 } catch (IOException e) { 1761 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1762 } 1763 } 1764 } 1765 1766 class WplHandler implements ElementListener { 1767 1768 final ContentHandler handler; 1769 String playListDirectory; 1770 1771 public WplHandler(String playListDirectory, Uri uri, Cursor fileList) { 1772 this.playListDirectory = playListDirectory; 1773 1774 RootElement root = new RootElement("smil"); 1775 Element body = root.getChild("body"); 1776 Element seq = body.getChild("seq"); 1777 Element media = seq.getChild("media"); 1778 media.setElementListener(this); 1779 1780 this.handler = root.getContentHandler(); 1781 } 1782 1783 @Override 1784 public void start(Attributes attributes) { 1785 String path = attributes.getValue("", "src"); 1786 if (path != null) { 1787 cachePlaylistEntry(path, playListDirectory); 1788 } 1789 } 1790 1791 @Override 1792 public void end() { 1793 } 1794 1795 ContentHandler getContentHandler() { 1796 return handler; 1797 } 1798 } 1799 1800 private void processWplPlayList(String path, String playListDirectory, Uri uri, 1801 ContentValues values, Cursor fileList) { 1802 FileInputStream fis = null; 1803 try { 1804 File f = new File(path); 1805 if (f.exists()) { 1806 fis = new FileInputStream(f); 1807 1808 mPlaylistEntries.clear(); 1809 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), 1810 new WplHandler(playListDirectory, uri, fileList).getContentHandler()); 1811 1812 processCachedPlaylist(fileList, values, uri); 1813 } 1814 } catch (SAXException e) { 1815 e.printStackTrace(); 1816 } catch (IOException e) { 1817 e.printStackTrace(); 1818 } finally { 1819 try { 1820 if (fis != null) 1821 fis.close(); 1822 } catch (IOException e) { 1823 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1824 } 1825 } 1826 } 1827 1828 private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException { 1829 String path = entry.mPath; 1830 ContentValues values = new ContentValues(); 1831 int lastSlash = path.lastIndexOf('/'); 1832 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1833 Uri uri, membersUri; 1834 long rowId = entry.mRowId; 1835 1836 // make sure we have a name 1837 String name = values.getAsString(MediaStore.Audio.Playlists.NAME); 1838 if (name == null) { 1839 name = values.getAsString(MediaStore.MediaColumns.TITLE); 1840 if (name == null) { 1841 // extract name from file name 1842 int lastDot = path.lastIndexOf('.'); 1843 name = (lastDot < 0 ? path.substring(lastSlash + 1) 1844 : path.substring(lastSlash + 1, lastDot)); 1845 } 1846 } 1847 1848 values.put(MediaStore.Audio.Playlists.NAME, name); 1849 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1850 1851 if (rowId == 0) { 1852 values.put(MediaStore.Audio.Playlists.DATA, path); 1853 uri = mMediaProvider.insert(mPlaylistsUri, values); 1854 rowId = ContentUris.parseId(uri); 1855 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1856 } else { 1857 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1858 mMediaProvider.update(uri, values, null, null); 1859 1860 // delete members of existing playlist 1861 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1862 mMediaProvider.delete(membersUri, null, null); 1863 } 1864 1865 String playListDirectory = path.substring(0, lastSlash + 1); 1866 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1867 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1868 1869 if (fileType == MediaFile.FILE_TYPE_M3U) { 1870 processM3uPlayList(path, playListDirectory, membersUri, values, fileList); 1871 } else if (fileType == MediaFile.FILE_TYPE_PLS) { 1872 processPlsPlayList(path, playListDirectory, membersUri, values, fileList); 1873 } else if (fileType == MediaFile.FILE_TYPE_WPL) { 1874 processWplPlayList(path, playListDirectory, membersUri, values, fileList); 1875 } 1876 } 1877 1878 private void processPlayLists() throws RemoteException { 1879 Iterator<FileEntry> iterator = mPlayLists.iterator(); 1880 Cursor fileList = null; 1881 try { 1882 // use the files uri and projection because we need the format column, 1883 // but restrict the query to just audio files 1884 fileList = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION, 1885 "media_type=2", null, null, null); 1886 while (iterator.hasNext()) { 1887 FileEntry entry = iterator.next(); 1888 // only process playlist files if they are new or have been modified since the last scan 1889 if (entry.mLastModifiedChanged) { 1890 processPlayList(entry, fileList); 1891 } 1892 } 1893 } catch (RemoteException e1) { 1894 } finally { 1895 if (fileList != null) { 1896 fileList.close(); 1897 } 1898 } 1899 } 1900 1901 private native void processDirectory(String path, MediaScannerClient client); 1902 private native boolean processFile(String path, String mimeType, MediaScannerClient client); 1903 private native void setLocale(String locale); 1904 1905 public native byte[] extractAlbumArt(FileDescriptor fd); 1906 1907 private static native final void native_init(); 1908 private native final void native_setup(); 1909 private native final void native_finalize(); 1910 1911 @Override 1912 public void close() { 1913 mCloseGuard.close(); 1914 if (mClosed.compareAndSet(false, true)) { 1915 mMediaProvider.close(); 1916 native_finalize(); 1917 } 1918 } 1919 1920 @Override 1921 protected void finalize() throws Throwable { 1922 try { 1923 if (mCloseGuard != null) { 1924 mCloseGuard.warnIfOpen(); 1925 } 1926 1927 close(); 1928 } finally { 1929 super.finalize(); 1930 } 1931 } 1932 } 1933